


/*-------------------------------------------------------------------------
 *
 * indexcmds.c
 *	  PostgreSQL 定义和删除索引的代码。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/commands/indexcmds.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include "access/amapi.h"
#include "access/heapam.h"
#include "access/htup_details.h"
#include "access/reloptions.h"
#include "access/sysattr.h"
#include "access/tableam.h"
#include "access/xact.h"
#include "catalog/catalog.h"
#include "catalog/index.h"
#include "catalog/indexing.h"
#include "catalog/pg_am.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_constraint.h"
#include "catalog/pg_inherits.h"
#include "catalog/pg_opclass.h"
#include "catalog/pg_opfamily.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_type.h"
#include "commands/comment.h"
#include "commands/dbcommands.h"
#include "commands/defrem.h"
#include "commands/event_trigger.h"
#include "commands/progress.h"
#include "commands/tablecmds.h"
#include "commands/tablespace.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/optimizer.h"
#include "parser/parse_coerce.h"
#include "parser/parse_func.h"
#include "parser/parse_oper.h"
#include "partitioning/partdesc.h"
#include "pgstat.h"
#include "rewrite/rewriteManip.h"
#include "storage/lmgr.h"
#include "storage/proc.h"
#include "storage/procarray.h"
#include "storage/sinvaladt.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
#include "utils/inval.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/partcache.h"
#include "utils/pg_rusage.h"
#include "utils/regproc.h"
#include "utils/snapmgr.h"
#include "utils/syscache.h"


/* 非导出函数原型 */
static bool fc_CompareOpclassOptions(Datum *fc_opts1, Datum *fc_opts2, int fc_natts);
static void fc_CheckPredicate(Expr *fc_predicate);
static void fc_ComputeIndexAttrs(IndexInfo *fc_indexInfo,
							  Oid *fc_typeOidP,
							  Oid *fc_collationOidP,
							  Oid *fc_classOidP,
							  int16 *fc_colOptionP,
							  List *fc_attList,
							  List *fc_exclusionOpNames,
							  Oid fc_relId,
							  const char *fc_accessMethodName, Oid fc_accessMethodId,
							  bool fc_amcanorder,
							  bool fc_isconstraint,
							  Oid fc_ddl_userid,
							  int fc_ddl_sec_context,
							  int *fc_ddl_save_nestlevel);
static char *fc_ChooseIndexName(const char *fc_tabname, Oid fc_namespaceId,
							 List *fc_colnames, List *fc_exclusionOpNames,
							 bool fc_primary, bool fc_isconstraint);
static char *fc_ChooseIndexNameAddition(List *fc_colnames);
static List *fc_ChooseIndexColumnNames(List *fc_indexElems);
static void fc_ReindexIndex(RangeVar *fc_indexRelation, ReindexParams *fc_params,
						 bool fc_isTopLevel);
static void fc_RangeVarCallbackForReindexIndex(const RangeVar *fc_relation,
											Oid fc_relId, Oid fc_oldRelId, void *fc_arg);
static Oid	fc_ReindexTable(RangeVar *fc_relation, ReindexParams *fc_params,
						 bool fc_isTopLevel);
static void fc_ReindexMultipleTables(const char *fc_objectName,
								  ReindexObjectType fc_objectKind, ReindexParams *fc_params);
static void fc_reindex_error_callback(void *fc_args);
static void fc_ReindexPartitions(Oid fc_relid, ReindexParams *fc_params,
							  bool fc_isTopLevel);
static void fc_ReindexMultipleInternal(List *fc_relids,
									ReindexParams *fc_params);
static bool fc_ReindexRelationConcurrently(Oid fc_relationOid,
										ReindexParams *fc_params);
static void fc_update_relispartition(Oid fc_relationId, bool fc_newval);
static inline void fc_set_indexsafe_procflags(void);

/*
 * RangeVarCallbackForReindexIndex() 的回调参数类型
 */
struct ReindexIndexCallbackState
{
	ReindexParams params;		/* 语句中的选项 */
	Oid			locked_table_oid;	/* 跟踪之前锁定的表 */
};

/*
 * reindex_error_callback() 的回调参数
 */
typedef struct ReindexErrorInfo
{
	char	   *relname;
	char	   *relnamespace;
	char		relkind;
} ReindexErrorInfo;

/*
 * CheckIndexCompatible
 *		确定现有索引定义是否与潜在的索引定义兼容，以便现有的索引存储
 *		可以作为新索引的存储，避免重建。
 *
 * 'oldId': 现有索引的 OID
 * 'accessMethodName': 要使用的 AM 的名称。
 * 'attributeList': 一个 IndexElem 列表，指定要索引的列和表达式。
 * 'exclusionOpNames': 排除约束运算符的名称列表，
 *		如果不是排除约束则为 NIL。
 *
 * 这适用于 ALTER TABLE ALTER TYPE 的需求，该操作重新创建
 * 任何依赖于更改列的索引，基于它们的 pg_get_indexdef
 * 或 pg_get_constraintdef 定义。我们省略了
 * DefineIndex 的一些合理性检查。我们假定旧索引和新索引具有相同的列数，
 * 并且如果有一个包含表达式或谓词的列，两个索引都有。
 * 属性列表引发的错误仍然适用。
 *
 * 大多数可以跳过表重写的列类型更改不会使索引失效。
 * 当所有运算符类、排序规则和排除运算符匹配时，我们承认这一点。
 * 虽然我们可以进一步允许 btree 和 hash 索引的内部操作族变化，
 * 但这带来了微妙的复杂性，而对核心类型没有具体的好处。请注意，INCLUDE 列
 * 不会被此函数检查， 对它们而言，跳过表重写就足够了。
 *
 * 当比较或排除运算符具有多态输入类型时，实际输入类型也必须匹配。
 * 这防止了运算符根据 get_fn_expr_argtype() 变化行为的可能性。
 * 目前，这一风险仍然是理论上的：check_exclusion_constraint() 和
 * 所有核心索引访问方法拒绝为此类调用设置 fn_expr。
 *
 * 我们尚未实现测试以验证表达式列或谓词的兼容性，因此假设任何此类索引都是不兼容的。
 */
bool CheckIndexCompatible(Oid fc_oldId,
					 const char *fc_accessMethodName,
					 List *fc_attributeList,
					 List *fc_exclusionOpNames)
{
	bool		fc_isconstraint;
	Oid		   *fc_typeObjectId;
	Oid		   *fc_collationObjectId;
	Oid		   *fc_classObjectId;
	Oid			fc_accessMethodId;
	Oid			fc_relationId;
	HeapTuple	fc_tuple;
	Form_pg_index fc_indexForm;
	Form_pg_am	fc_accessMethodForm;
	IndexAmRoutine *fc_amRoutine;
	bool		fc_amcanorder;
	int16	   *fc_coloptions;
	IndexInfo  *fc_indexInfo;
	int			fc_numberOfAttributes;
	int			fc_old_natts;
	bool		fc_isnull;
	bool		fc_ret = true;
	oidvector  *fc_old_indclass;
	oidvector  *fc_old_indcollation;
	Relation	fc_irel;
	int			fc_i;
	Datum		fc_d;

	/* 调用方应该已经以某种方式锁定了关系。 */
	fc_relationId = IndexGetRelation(fc_oldId, false);

	/*
	 * 我们可以无条件地假装 isconstraint = false。它仅用于
	 * 决定我们绝不会发生的错误消息的文本。
	 */
	fc_isconstraint = false;

	fc_numberOfAttributes = list_length(fc_attributeList);
	Assert(fc_numberOfAttributes > 0);
	Assert(fc_numberOfAttributes <= INDEX_MAX_KEYS);

	/* 查找访问方法 */
	fc_tuple = SearchSysCache1(AMNAME, PointerGetDatum(fc_accessMethodName));
	if (!HeapTupleIsValid(fc_tuple))
		ereport(ERROR,
				(errcode(ERRCODE_UNDEFINED_OBJECT),
				 errmsg("access method \"%s\" does not exist",
						fc_accessMethodName)));
	fc_accessMethodForm = (Form_pg_am) GETSTRUCT(fc_tuple);
	fc_accessMethodId = fc_accessMethodForm->oid;
	fc_amRoutine = GetIndexAmRoutine(fc_accessMethodForm->amhandler);
	ReleaseSysCache(fc_tuple);

	fc_amcanorder = fc_amRoutine->amcanorder;

	/*
	 * 计算新索引的运算符类、排序规则和排除运算符，以便我们可以测试它是否与现有索引兼容。
	 * 请注意，ComputeIndexAttrs 在这里可能会失败，但这没关系：
	 * DefineIndex 之后也会失败。我们的 attributeList 仅包含
	 * 键属性，因此我们正在用相同的值填充 ii_NumIndexAttrs 和
	 * ii_NumIndexKeyAttrs。
	 */
	fc_indexInfo = makeIndexInfo(fc_numberOfAttributes, fc_numberOfAttributes,
							  fc_accessMethodId, NIL, NIL, false, false, false, false);
	fc_typeObjectId = (Oid *) palloc(fc_numberOfAttributes * sizeof(Oid));
	fc_collationObjectId = (Oid *) palloc(fc_numberOfAttributes * sizeof(Oid));
	fc_classObjectId = (Oid *) palloc(fc_numberOfAttributes * sizeof(Oid));
	fc_coloptions = (int16 *) palloc(fc_numberOfAttributes * sizeof(int16));
	fc_ComputeIndexAttrs(fc_indexInfo,
					  fc_typeObjectId, fc_collationObjectId, fc_classObjectId,
					  fc_coloptions, fc_attributeList,
					  fc_exclusionOpNames, fc_relationId,
					  fc_accessMethodName, fc_accessMethodId,
					  fc_amcanorder, fc_isconstraint, InvalidOid, 0, NULL);


	/* 获取即将过时的 pg_index 元组。 */
	fc_tuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(fc_oldId));
	if (!HeapTupleIsValid(fc_tuple))
		elog(ERROR, "cache lookup failed for index %u", fc_oldId);
	fc_indexForm = (Form_pg_index) GETSTRUCT(fc_tuple);

	/*
	 * 我们不评估表达式或谓词；假设不兼容。
	 * 此外，如果索引因任何原因无效，将其视为不兼容。
	 */
	if (!(heap_attisnull(fc_tuple, Anum_pg_index_indpred, NULL) &&
		  heap_attisnull(fc_tuple, Anum_pg_index_indexprs, NULL) &&
		  fc_indexForm->indisvalid))
	{
		ReleaseSysCache(fc_tuple);
		return false;
	}

	/* 任何操作类或排序规则的更改都会破坏兼容性。 */
	fc_old_natts = fc_indexForm->indnkeyatts;
	Assert(fc_old_natts == fc_numberOfAttributes);

	fc_d = SysCacheGetAttr(INDEXRELID, fc_tuple, Anum_pg_index_indcollation, &fc_isnull);
	Assert(!fc_isnull);
	fc_old_indcollation = (oidvector *) DatumGetPointer(fc_d);

	fc_d = SysCacheGetAttr(INDEXRELID, fc_tuple, Anum_pg_index_indclass, &fc_isnull);
	Assert(!fc_isnull);
	fc_old_indclass = (oidvector *) DatumGetPointer(fc_d);

	fc_ret = (memcmp(fc_old_indclass->values, fc_classObjectId,
				  fc_old_natts * sizeof(Oid)) == 0 &&
		   memcmp(fc_old_indcollation->values, fc_collationObjectId,
				  fc_old_natts * sizeof(Oid)) == 0);

	ReleaseSysCache(fc_tuple);

	if (!fc_ret)
		return false;

	/* 对于多态opcintype，列类型的更改会破坏兼容性。 */
	fc_irel = index_open(fc_oldId, AccessShareLock);	/* 调用者可能有一个锁。 */
	for (fc_i = 0; fc_i < fc_old_natts; fc_i++)
	{
		if (IsPolymorphicType(get_opclass_input_type(fc_classObjectId[fc_i])) &&
			TupleDescAttr(fc_irel->rd_att, fc_i)->atttypid != fc_typeObjectId[fc_i])
		{
			fc_ret = false;
			break;
		}
	}

	/* 任何操作类选项的更改都会破坏兼容性。 */
	if (fc_ret)
	{
		Datum	   *fc_opclassOptions = RelationGetIndexRawAttOptions(fc_irel);

		fc_ret = fc_CompareOpclassOptions(fc_opclassOptions,
									fc_indexInfo->ii_OpclassOptions, fc_old_natts);

		if (fc_opclassOptions)
			pfree(fc_opclassOptions);
	}

	/* 任何排除操作符选择的更改都会破坏兼容性。 */
	if (fc_ret && fc_indexInfo->ii_ExclusionOps != NULL)
	{
		Oid		   *fc_old_operators,
				   *fc_old_procs;
		uint16	   *fc_old_strats;

		RelationGetExclusionInfo(fc_irel, &fc_old_operators, &fc_old_procs, &fc_old_strats);
		fc_ret = memcmp(fc_old_operators, fc_indexInfo->ii_ExclusionOps,
					 fc_old_natts * sizeof(Oid)) == 0;

		/* 对于多态运算符，要求精确的输入类型匹配。 */
		if (fc_ret)
		{
			for (fc_i = 0; fc_i < fc_old_natts && fc_ret; fc_i++)
			{
				Oid			fc_left,
							fc_right;

				op_input_types(fc_indexInfo->ii_ExclusionOps[fc_i], &fc_left, &fc_right);
				if ((IsPolymorphicType(fc_left) || IsPolymorphicType(fc_right)) &&
					TupleDescAttr(fc_irel->rd_att, fc_i)->atttypid != fc_typeObjectId[fc_i])
				{
					fc_ret = false;
					break;
				}
			}
		}
	}

	index_close(fc_irel, NoLock);
	return fc_ret;
}

/*
 * CompareOpclassOptions
 *
 * 比较每列的操作类选项，这些选项由text[]
 * datums的数组表示。数组的元素和数组本身都可以为NULL。
 */
static bool fc_CompareOpclassOptions(Datum *fc_opts1, Datum *fc_opts2, int fc_natts)
{
	int			fc_i;
	FmgrInfo	fc_fm;

	if (!fc_opts1 && !fc_opts2)
		return true;

	fmgr_info(F_ARRAY_EQ, &fc_fm);
	for (fc_i = 0; fc_i < fc_natts; fc_i++)
	{
		Datum		fc_opt1 = fc_opts1 ? fc_opts1[fc_i] : (Datum) 0;
		Datum		fc_opt2 = fc_opts2 ? fc_opts2[fc_i] : (Datum) 0;

		if (fc_opt1 == (Datum) 0)
		{
			if (fc_opt2 == (Datum) 0)
				continue;
			else
				return false;
		}
		else if (fc_opt2 == (Datum) 0)
			return false;

		/*
		 * 比较非NULL的text[] datums。使用C排序规则强制文本的二进制
		 * 等价性，因为我们对操作类选项的语义一无所知。
		 */
		if (!DatumGetBool(FunctionCall2Coll(&fc_fm, C_COLLATION_OID, fc_opt1, fc_opt2)))
			return false;
	}

	return true;
}

/*
 * WaitForOlderSnapshots
 *
 * 等待可能比给定xmin限制有更旧快照的事务，因为它可能
 * 不包含在快照生成之前刚被删除的元组。获取这些事务的
 * VXIDs列表，并分别等待它们。这在构建索引时使用
 * 并发进行。
 *
 * 我们可以排除任何xmin > 给定xmin的正在运行的事务；
 * 它们的最旧快照一定比我们的xmin限制要新。
 * 我们还可以排除任何xmin =零的事务，因为它们显然
 * 根本没有活动快照（而它们可能正在进行的任何快照
 * 无疑比我们要新）。其他数据库中的事务也可以忽略，
 * 因为它们根本无法看到正在处理的索引。
 *
 * 我们还可以排除自动真空过程和运行手动
 * 延迟VACUUM的过程，因为它们也不会因为缺失的索引条目
 * 而受到影响。（然而，手动ANALYZE不能被排除，因为
 * 它们可能处于将要执行任意操作的事务中。）在非表达式
 * 或部分的索引上并发运行CREATE INDEX或REINDEX
 * 的过程也可以安全忽略，因为我们知道这些过程不会检查
 * 它们正在建立索引的表外的任何数据。
 *
 * 此外，GetCurrentVirtualXIDs永远不会报告我们自己的vxid，
 * 因此我们不需要检查这一点。
 *
 * 如果一个进程在xmin为零时处于事务空闲状态，根据上述
 * 论点，我们不再需要等待它。我们目前没有基础设施
 * 在发生这种情况时停止等待，但至少我们可以避免在
 * 开始等待时它处于空闲状态的愚蠢。我们通过反复重新检查
 * GetCurrentVirtualXIDs的输出来实现。 如果在任何一次迭代中，
 * 某个特定的vxid没有出现在输出中，我们就知道可以
 * 忘记它。
 */
void WaitForOlderSnapshots(TransactionId fc_limitXmin, bool fc_progress)
{
	int			fc_n_old_snapshots;
	int			fc_i;
	VirtualTransactionId *fc_old_snapshots;

	fc_old_snapshots = GetCurrentVirtualXIDs(fc_limitXmin, true, false,
										  PROC_IS_AUTOVACUUM | PROC_IN_VACUUM
										  | PROC_IN_SAFE_IC,
										  &fc_n_old_snapshots);
	if (fc_progress)
		pgstat_progress_update_param(PROGRESS_WAITFOR_TOTAL, fc_n_old_snapshots);

	for (fc_i = 0; fc_i < fc_n_old_snapshots; fc_i++)
	{
		if (!VirtualTransactionIdIsValid(fc_old_snapshots[fc_i]))
			continue;			/* 在先前周期中发现不感兴趣 */

		if (fc_i > 0)
		{
			/* 查看是否有任何变化... */
			VirtualTransactionId *fc_newer_snapshots;
			int			fc_n_newer_snapshots;
			int			fc_j;
			int			fc_k;

			fc_newer_snapshots = GetCurrentVirtualXIDs(fc_limitXmin,
													true, false,
													PROC_IS_AUTOVACUUM | PROC_IN_VACUUM
													| PROC_IN_SAFE_IC,
													&fc_n_newer_snapshots);
			for (fc_j = fc_i; fc_j < fc_n_old_snapshots; fc_j++)
			{
				if (!VirtualTransactionIdIsValid(fc_old_snapshots[fc_j]))
					continue;	/* 在先前周期中发现不感兴趣 */
				for (fc_k = 0; fc_k < fc_n_newer_snapshots; fc_k++)
				{
					if (VirtualTransactionIdEquals(fc_old_snapshots[fc_j],
												   fc_newer_snapshots[fc_k]))
						break;
				}
				if (fc_k >= fc_n_newer_snapshots) /* 不再存在 */
					SetInvalidVirtualTransactionId(fc_old_snapshots[fc_j]);
			}
			pfree(fc_newer_snapshots);
		}

		if (VirtualTransactionIdIsValid(fc_old_snapshots[fc_i]))
		{
			/* 如果请求，发布我们要等待谁。 */
			if (fc_progress)
			{
				PGPROC	   *fc_holder = BackendIdGetProc(fc_old_snapshots[fc_i].backendId);

				if (fc_holder)
					pgstat_progress_update_param(PROGRESS_WAITFOR_CURRENT_PID,
												 fc_holder->pid);
			}
			VirtualXactLock(fc_old_snapshots[fc_i], true);
		}

		if (fc_progress)
			pgstat_progress_update_param(PROGRESS_WAITFOR_DONE, fc_i + 1);
	}
}


/*
 * DefineIndex
 *		创建一个新的索引。
 *
 * 此函数根据 pg_dump 的需要管理当前用户 ID。 
 * 在新数据库中重新创建旧数据库目录条目是可以的，无论
 * 现在哪些用户有权限重新创建这些条目。这
 * 仅仅是状态的保留。以对象所有者以外的任何身份运行不透明表达式，例如调用
 * 在目录条目中命名的函数或评估目录
 * 条目中的 pg_node_tree，是不可以的。为了遵循这些原则并保持安全，无论如何都要使用
 * 表所有者用户 ID 进行大多数 ACL 检查。对于那些没有
 * 遍历不透明表达式到达的 ACL 检查，使用原始用户 ID。（pg_dump 可以从
 * 目录中预测这样的 ACL 检查。）总体而言，这是一团糟。未来的 DDL 开发应该
 * 考虑提供一个用于目录设置的 DDL 命令和一个用于运行不透明表达式的步骤的单独 DDL
 * 命令。
 *
 * 'relationId': 要在其上创建索引的堆关系的 OID
 * 'stmt': 描述新索引属性的 IndexStmt。
 * 'indexRelationId': 通常为 InvalidOid，但在引导过程中可以是
 *		非零值，以指定索引的预选 OID。
 * 'parentIndexId': 父索引的 OID；如果不是分区索引的子索引，则为 InvalidOid。
 * 'parentConstraintId': 父约束的 OID；如果不是约束的子约束，则为 InvalidOid
 *		（仅在递归时使用）
 * 'is_alter_table': 这是由于 ALTER 而非 CREATE 操作。
 * 'check_rights': 检查命名空间和表空间中的 CREATE 权限。 （这
 *		应该是正确的，除非 ALTER 正在删除/重新创建索引。）
 * 'check_not_in_use': 检查表在当前会话中未被使用。
 *		这应该是正确的，除非调用者持有该表打开，在这种情况下，调用者最好早些检查。
 * 'skip_build': 制作目录条目，但不创建索引文件。
 * 'quiet': 抑制通常提供的约束通知聊天。
 *
 * 返回创建的索引的对象地址。
 */
ObjectAddress DefineIndex(Oid fc_relationId,
			IndexStmt *fc_stmt,
			Oid fc_indexRelationId,
			Oid fc_parentIndexId,
			Oid fc_parentConstraintId,
			bool fc_is_alter_table,
			bool fc_check_rights,
			bool fc_check_not_in_use,
			bool fc_skip_build,
			bool fc_quiet)
{
	bool		fc_concurrent;
	char	   *fc_indexRelationName;
	char	   *fc_accessMethodName;
	Oid		   *fc_typeObjectId;
	Oid		   *fc_collationObjectId;
	Oid		   *fc_classObjectId;
	Oid			fc_accessMethodId;
	Oid			fc_namespaceId;
	Oid			fc_tablespaceId;
	Oid			fc_createdConstraintId = InvalidOid;
	List	   *fc_indexColNames;
	List	   *fc_allIndexParams;
	Relation	fc_rel;
	HeapTuple	fc_tuple;
	Form_pg_am	fc_accessMethodForm;
	IndexAmRoutine *fc_amRoutine;
	bool		fc_amcanorder;
	amoptions_function fc_amoptions;
	bool		fc_partitioned;
	bool		fc_safe_index;
	Datum		fc_reloptions;
	int16	   *fc_coloptions;
	IndexInfo  *fc_indexInfo;
	bits16		fc_flags;
	bits16		fc_constr_flags;
	int			fc_numberOfAttributes;
	int			fc_numberOfKeyAttributes;
	TransactionId fc_limitXmin;
	ObjectAddress fc_address;
	LockRelId	fc_heaprelid;
	LOCKTAG		fc_heaplocktag;
	LOCKMODE	fc_lockmode;
	Snapshot	fc_snapshot;
	Oid			fc_root_save_userid;
	int			fc_root_save_sec_context;
	int			fc_root_save_nestlevel;
	int			fc_i;

	fc_root_save_nestlevel = NewGUCNestLevel();

	/*
	 * 一些调用者需要我们使用一个空的 default_tablespace；这是一个
	 * 必要的 hack，以便能够在
	 * 表重写 ALTER TABLE 后准确地重现目录状态。
	 */
	if (fc_stmt->reset_default_tblspc)
		(void) set_config_option("default_tablespace", "",
								 PGC_USERSET, PGC_S_SESSION,
								 GUC_ACTION_SAVE, true, 0, false);

	/*
	 * 在临时关系上强制非并发构建，即使请求了 CONCURRENTLY。
	 * 其他后端无法访问临时关系，因此在获取更强的锁时没有任何损害，非并发 DROP
	 * 更有效。在执行任何使用并发选项的操作之前，执行此操作。
	 */
	if (fc_stmt->concurrent && get_rel_persistence(fc_relationId) != RELPERSISTENCE_TEMP)
		fc_concurrent = true;
	else
		fc_concurrent = false;

	/*
	 * 开始进度报告。如果我们正在构建一个分区，这已经
	 * 完成。
	 */
	if (!OidIsValid(fc_parentIndexId))
	{
		pgstat_progress_start_command(PROGRESS_COMMAND_CREATE_INDEX,
									  fc_relationId);
		pgstat_progress_update_param(PROGRESS_CREATEIDX_COMMAND,
									 fc_concurrent ?
									 PROGRESS_CREATEIDX_COMMAND_CREATE_CONCURRENTLY :
									 PROGRESS_CREATEIDX_COMMAND_CREATE);
	}

	/*
	 * 尚无索引 OID 可报告
	 */
	pgstat_progress_update_param(PROGRESS_CREATEIDX_INDEX_OID,
								 InvalidOid);

	/*
	 * 计算索引中的关键属性
	 */
	fc_numberOfKeyAttributes = list_length(fc_stmt->indexParams);

	/*
	 * 计算新索引列的列表，包括主键列和
	 * INCLUDE 列。稍后我们可以通过检查
	 * 列表位置来确定哪些是主键列，哪些只是
	 * INCLUDE 列表的一部分。在位置小于 ii_NumIndexKeyAttrs 的
	 * 列表项是主键列，等于或大于的任何项是
	 * INCLUDE 列。
	 */
	fc_allIndexParams = list_concat_copy(fc_stmt->indexParams,
									  fc_stmt->indexIncludingParams);
	fc_numberOfAttributes = list_length(fc_allIndexParams);

	if (fc_numberOfKeyAttributes <= 0)
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
				 errmsg("must specify at least one column")));
	if (fc_numberOfAttributes > INDEX_MAX_KEYS)
		ereport(ERROR,
				(errcode(ERRCODE_TOO_MANY_COLUMNS),
				 errmsg("cannot use more than %d columns in an index",
						INDEX_MAX_KEYS)));

	/*
	 * 在进行标准索引构建时，只允许 SELECT ... FOR UPDATE/SHARE；
	 * 但对于并发构建，我们允许 INSERT/UPDATE/DELETE
	 * （但不允许 VACUUM）。
	 *
	 * 注意：调用者有责任确保 relationId 指向
	 * 应该建立索引的关系；除非在引导模式下，
	 * 否则这通常要求调用者已经锁定了
	 * 该关系。为了避免锁升级危险，那个锁应该
	 * 至少和我们在这里所采用的锁一样强。
	 *
	 * 注意：如果这里的锁强度发生变化，由某些特定的
	 * ambuild 函数控制的并行工作者运行的代码
	 * 也需要进行更新。
	 */
	fc_lockmode = fc_concurrent ? ShareUpdateExclusiveLock : ShareLock;
	fc_rel = table_open(fc_relationId, fc_lockmode);

	/*
	 * 切换到表所有者的用户 ID，以便任何索引函数都以该用户身份运行。
	 * 同时锁定安全受限的操作。我们已经安排了
	 * 使 GUC 变量更改局限于此命令。
	 */
	GetUserIdAndSecContext(&fc_root_save_userid, &fc_root_save_sec_context);
	SetUserIdAndSecContext(fc_rel->rd_rel->relowner,
						   fc_root_save_sec_context | SECURITY_RESTRICTED_OPERATION);

	fc_namespaceId = RelationGetNamespace(fc_rel);

	/* 确保对这种关系建立索引是合适的 */
	switch (fc_rel->rd_rel->relkind)
	{
		case RELKIND_RELATION:
		case RELKIND_MATVIEW:
		case RELKIND_PARTITIONED_TABLE:
			/* 好的 */
			break;
		default:
			ereport(ERROR,
					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
					 errmsg("cannot create index on relation \"%s\"",
							RelationGetRelationName(fc_rel)),
					 errdetail_relkind_not_supported(fc_rel->rd_rel->relkind)));
			break;
	}

	/*
	 * 为分区表建立行为，并验证参数的合理性。
	 *
	 * 在这种情况下，我们不构建实际的索引；我们只创建几个
	 * 目录条目。实际的索引通过递归每个
	 * 分区来构建。
	 */
	fc_partitioned = fc_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE;
	if (fc_partitioned)
	{
		/*
		 * 注意：我们检查 'stmt->concurrent' 而不是 'concurrent'，以便
		 * 针对临时表也会引发错误。即使我们可以在临时表上这样做
		 * 因为我们并没有真正地并发执行，感觉保持一致更好。
		 */
		if (fc_stmt->concurrent)
			ereport(ERROR,
					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
					 errmsg("cannot create index on partitioned table \"%s\" concurrently",
							RelationGetRelationName(fc_rel))));
		if (fc_stmt->excludeOpNames)
			ereport(ERROR,
					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
					 errmsg("cannot create exclusion constraints on partitioned table \"%s\"",
							RelationGetRelationName(fc_rel))));
	}

	/*
	 * 不要尝试在其他后端的临时表上创建索引。
	 */
	if (RELATION_IS_OTHER_TEMP(fc_rel))
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("cannot create indexes on temporary tables of other sessions")));

	/*
	 * 除非我们的调用者已确保已检查此项，否则坚持
	 * 该表也不能被我们的会话使用。否则我们可能
	 * 无法在新索引中建立条目（例如，如果
	 * INSERT 或 UPDATE 正在进行中并已生成其目标索引的列表）。
	 */
	if (fc_check_not_in_use)
		CheckTableNotInUse(fc_rel, "CREATE INDEX");

	/*
	 * 验证我们（仍然）在关系的命名空间中具有创建权限。
	 * （假设在创建关系时我们有，但现在可能没有。）
	 * 如果调用者不想检查则跳过检查。如果
	 * 是引导过程，则也跳过检查，因为权限机制可能
	 * 尚未工作。
	 */
	if (fc_check_rights && !IsBootstrapProcessingMode())
	{
		AclResult	fc_aclresult;

		fc_aclresult = pg_namespace_aclcheck(fc_namespaceId, fc_root_save_userid,
										  ACL_CREATE);
		if (fc_aclresult != ACLCHECK_OK)
			aclcheck_error(fc_aclresult, OBJECT_SCHEMA,
						   get_namespace_name(fc_namespaceId));
	}

	/*
	 * 选择要使用的表空间。如果未指定，则使用默认表空间
	 * （这可能又默认为数据库的默认表空间）。
	 */
	if (fc_stmt->tableSpace)
	{
		fc_tablespaceId = get_tablespace_oid(fc_stmt->tableSpace, false);
		if (fc_partitioned && fc_tablespaceId == MyDatabaseTableSpace)
			ereport(ERROR,
					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
					 errmsg("cannot specify default tablespace for partitioned relations")));
	}
	else
	{
		fc_tablespaceId = GetDefaultTablespace(fc_rel->rd_rel->relpersistence,
											fc_partitioned);
		/* 注意在这种情况下 InvalidOid 是可以的 */
	}

	/* 检查表空间权限 */
	if (fc_check_rights &&
		OidIsValid(fc_tablespaceId) && fc_tablespaceId != MyDatabaseTableSpace)
	{
		AclResult	fc_aclresult;

		fc_aclresult = pg_tablespace_aclcheck(fc_tablespaceId, fc_root_save_userid,
										   ACL_CREATE);
		if (fc_aclresult != ACLCHECK_OK)
			aclcheck_error(fc_aclresult, OBJECT_TABLESPACE,
						   get_tablespace_name(fc_tablespaceId));
	}

	/*
	 * 强制将共享索引放入 pg_global 表空间。这有点像是一种
	 * hack，但似乎比在 BKI 命令中标记它们要简单。另一方面，
	 * 如果不是共享的，则不允许将其放置在那里。
	 */
	if (fc_rel->rd_rel->relisshared)
		fc_tablespaceId = GLOBALTABLESPACE_OID;
	else if (fc_tablespaceId == GLOBALTABLESPACE_OID)
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
				 errmsg("only shared relations can be placed in pg_global tablespace")));

	/*
	 * 选择索引列名。
	 */
	fc_indexColNames = fc_ChooseIndexColumnNames(fc_allIndexParams);

	/*
	 * 如果调用者没有指定名称，则为索引选择名称。
	 */
	fc_indexRelationName = fc_stmt->idxname;
	if (fc_indexRelationName == NULL)
		fc_indexRelationName = fc_ChooseIndexName(RelationGetRelationName(fc_rel),
											fc_namespaceId,
											fc_indexColNames,
											fc_stmt->excludeOpNames,
											fc_stmt->primary,
											fc_stmt->isconstraint);

	/*
	 * 查找访问方法，验证其是否能够处理请求的功能。
	 */
	fc_accessMethodName = fc_stmt->accessMethod;
	fc_tuple = SearchSysCache1(AMNAME, PointerGetDatum(fc_accessMethodName));
	if (!HeapTupleIsValid(fc_tuple))
	{
		/*
		 * Hack 提供旧 RTREE 索引到 GiST 的或多或少透明的更新：
		 * 如果请求了 RTREE 而未找到，则使用 GIST。
		 */
		if (strcmp(fc_accessMethodName, "rtree") == 0)
		{
			ereport(NOTICE,
					(errmsg("substituting access method \"gist\" for obsolete method \"rtree\"")));
			fc_accessMethodName = "gist";
			fc_tuple = SearchSysCache1(AMNAME, PointerGetDatum(fc_accessMethodName));
		}

		if (!HeapTupleIsValid(fc_tuple))
			ereport(ERROR,
					(errcode(ERRCODE_UNDEFINED_OBJECT),
					 errmsg("access method \"%s\" does not exist",
							fc_accessMethodName)));
	}
	fc_accessMethodForm = (Form_pg_am) GETSTRUCT(fc_tuple);
	fc_accessMethodId = fc_accessMethodForm->oid;
	fc_amRoutine = GetIndexAmRoutine(fc_accessMethodForm->amhandler);

	pgstat_progress_update_param(PROGRESS_CREATEIDX_ACCESS_METHOD_OID,
								 fc_accessMethodId);

	if (fc_stmt->unique && !fc_amRoutine->amcanunique)
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("access method \"%s\" does not support unique indexes",
						fc_accessMethodName)));
	if (fc_stmt->indexIncludingParams != NIL && !fc_amRoutine->amcaninclude)
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("access method \"%s\" does not support included columns",
						fc_accessMethodName)));
	if (fc_numberOfKeyAttributes > 1 && !fc_amRoutine->amcanmulticol)
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("access method \"%s\" does not support multicolumn indexes",
						fc_accessMethodName)));
	if (fc_stmt->excludeOpNames && fc_amRoutine->amgettuple == NULL)
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("access method \"%s\" does not support exclusion constraints",
						fc_accessMethodName)));

	fc_amcanorder = fc_amRoutine->amcanorder;
	fc_amoptions = fc_amRoutine->amoptions;

	pfree(fc_amRoutine);
	ReleaseSysCache(fc_tuple);

	/*
	 * 验证谓词（如果提供的话）。
	 */
	if (fc_stmt->whereClause)
		fc_CheckPredicate((Expr *) fc_stmt->whereClause);

	/*
	 * 解析 AM 特定选项，转换为文本数组形式，验证。
	 */
	fc_reloptions = transformRelOptions((Datum) 0, fc_stmt->options,
									 NULL, NULL, false, false);

	(void) index_reloptions(fc_amoptions, fc_reloptions, true);

	/*
	 * 准备 index_create 的参数，主要是一个 IndexInfo 结构。
	 * 注意，谓词必须以隐式 AND 格式出现。在并发构建中，
	 * 标记为不准备插入。
	 */
	fc_indexInfo = makeIndexInfo(fc_numberOfAttributes,
							  fc_numberOfKeyAttributes,
							  fc_accessMethodId,
							  NIL,	/* 表达式，目前为 NIL */
							  make_ands_implicit((Expr *) fc_stmt->whereClause),
							  fc_stmt->unique,
							  fc_stmt->nulls_not_distinct,
							  !fc_concurrent,
							  fc_concurrent);

	fc_typeObjectId = (Oid *) palloc(fc_numberOfAttributes * sizeof(Oid));
	fc_collationObjectId = (Oid *) palloc(fc_numberOfAttributes * sizeof(Oid));
	fc_classObjectId = (Oid *) palloc(fc_numberOfAttributes * sizeof(Oid));
	fc_coloptions = (int16 *) palloc(fc_numberOfAttributes * sizeof(int16));
	fc_ComputeIndexAttrs(fc_indexInfo,
					  fc_typeObjectId, fc_collationObjectId, fc_classObjectId,
					  fc_coloptions, fc_allIndexParams,
					  fc_stmt->excludeOpNames, fc_relationId,
					  fc_accessMethodName, fc_accessMethodId,
					  fc_amcanorder, fc_stmt->isconstraint, fc_root_save_userid,
					  fc_root_save_sec_context, &fc_root_save_nestlevel);

	/*
	 * 创建 PRIMARY KEY 索引时的额外检查。
	 */
	if (fc_stmt->primary)
		index_check_primary_key(fc_rel, fc_indexInfo, fc_is_alter_table, fc_stmt);

	/*
	 * 如果该表是分区的并且我们正在创建唯一索引或主键，
	 * 确保分区键是索引列的子集。否则，通过将应该唯一的值
	 * 放在不同的分区中，将有可能违反唯一性。
	 *
	 * 如果我们有全局索引，可以取消此限制，但全局索引
	 * 本身也有问题，因此这是一个有用的特性组合。
	 */
	if (fc_partitioned && (fc_stmt->unique || fc_stmt->primary))
	{
		PartitionKey fc_key = RelationGetPartitionKey(fc_rel);
		const char *fc_constraint_type;
		int			fc_i;

		if (fc_stmt->primary)
			fc_constraint_type = "PRIMARY KEY";
		else if (fc_stmt->unique)
			fc_constraint_type = "UNIQUE";
		else if (fc_stmt->excludeOpNames != NIL)
			fc_constraint_type = "EXCLUDE";
		else
		{
			elog(ERROR, "unknown constraint type");
			fc_constraint_type = NULL; /* 保持编译器安静 */
		}

		/*
		 * 验证分区键中所有列都出现在唯一键定义中，且具有相同的
		 * 相等性概念。
		 */
		for (fc_i = 0; fc_i < fc_key->partnatts; fc_i++)
		{
			bool		fc_found = false;
			int			fc_eq_strategy;
			Oid			fc_ptkey_eqop;
			int			fc_j;

			/*
			 * 确定与此分区键列相关联的相等运算符。对于列表和范围
			 * 分区，分区键使用 btree 运算符类；哈希分区使用哈希
			 * 运算符类。（保持与 ComputePartitionAttrs 同步！）
			 */
			if (fc_key->strategy == PARTITION_STRATEGY_HASH)
				fc_eq_strategy = HTEqualStrategyNumber;
			else
				fc_eq_strategy = BTEqualStrategyNumber;

			fc_ptkey_eqop = get_opfamily_member(fc_key->partopfamily[fc_i],
											 fc_key->partopcintype[fc_i],
											 fc_key->partopcintype[fc_i],
											 fc_eq_strategy);
			if (!OidIsValid(fc_ptkey_eqop))
				elog(ERROR, "missing operator %d(%u,%u) in partition opfamily %u",
					 fc_eq_strategy, fc_key->partopcintype[fc_i], fc_key->partopcintype[fc_i],
					 fc_key->partopfamily[fc_i]);

			/*
			 * 我们也需要能够识别与索引列相关联的相等运算符。
			 * 我们知道如何处理 btree 运算符类；如果将来有其他
			 * 支持唯一索引的索引类型，则该逻辑需要扩展。
			 */
			if (fc_accessMethodId == BTREE_AM_OID)
				fc_eq_strategy = BTEqualStrategyNumber;
			else
				ereport(ERROR,
						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
						 errmsg("cannot match partition key to an index using access method \"%s\"",
								fc_accessMethodName)));

			/*
			 * 当分区键是表达式时，可能支持 UNIQUE 约束，但这值得吗？
			 * 暂时放弃。
			 */
			if (fc_key->partattrs[fc_i] == 0)
				ereport(ERROR,
						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
						 errmsg("unsupported %s constraint with partition key definition",
								fc_constraint_type),
						 errdetail("%s constraints cannot be used when partition keys include expressions.",
								   fc_constraint_type)));

			/* 搜索索引列以寻找匹配项 */
			for (fc_j = 0; fc_j < fc_indexInfo->ii_NumIndexKeyAttrs; fc_j++)
			{
				if (fc_key->partattrs[fc_i] == fc_indexInfo->ii_IndexAttrNumbers[fc_j])
				{
					/* 匹配了列，现在关于排序和相等运算符有什么问题？ */
					Oid			fc_idx_opfamily;
					Oid			fc_idx_opcintype;

					if (fc_key->partcollation[fc_i] != fc_collationObjectId[fc_j])
						continue;

					if (get_opclass_opfamily_and_input_type(fc_classObjectId[fc_j],
															&fc_idx_opfamily,
															&fc_idx_opcintype))
					{
						Oid			fc_idx_eqop;

						fc_idx_eqop = get_opfamily_member(fc_idx_opfamily,
													   fc_idx_opcintype,
													   fc_idx_opcintype,
													   fc_eq_strategy);
						if (fc_ptkey_eqop == fc_idx_eqop)
						{
							fc_found = true;
							break;
						}
					}
				}
			}

			if (!fc_found)
			{
				Form_pg_attribute fc_att;

				fc_att = TupleDescAttr(RelationGetDescr(fc_rel),
									fc_key->partattrs[fc_i] - 1);
				ereport(ERROR,
						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
						 errmsg("unique constraint on partitioned table must include all partitioning columns"),
						 errdetail("%s constraint on table \"%s\" lacks column \"%s\" which is part of the partition key.",
								   fc_constraint_type, RelationGetRelationName(fc_rel),
								   NameStr(fc_att->attname))));
			}
		}
	}


	/*
	 * 我们不允许在系统列上建立索引。它们不一定会被正确更新，
	 * 而且似乎也没有用处。
	 */
	for (fc_i = 0; fc_i < fc_indexInfo->ii_NumIndexAttrs; fc_i++)
	{
		AttrNumber	fc_attno = fc_indexInfo->ii_IndexAttrNumbers[fc_i];

		if (fc_attno < 0)
			ereport(ERROR,
					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
					 errmsg("index creation on system columns is not supported")));
	}

	/*
	 * 也检查在表达式或谓词中使用的系统列。
	 */
	if (fc_indexInfo->ii_Expressions || fc_indexInfo->ii_Predicate)
	{
		Bitmapset  *fc_indexattrs = NULL;

		pull_varattnos((Node *) fc_indexInfo->ii_Expressions, 1, &fc_indexattrs);
		pull_varattnos((Node *) fc_indexInfo->ii_Predicate, 1, &fc_indexattrs);

		for (fc_i = FirstLowInvalidHeapAttributeNumber + 1; fc_i < 0; fc_i++)
		{
			if (bms_is_member(fc_i - FirstLowInvalidHeapAttributeNumber,
							  fc_indexattrs))
				ereport(ERROR,
						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
						 errmsg("index creation on system columns is not supported")));
		}
	}

	/* 索引对其他人是否安全可忽略？请参见 set_indexsafe_procflags() */
	fc_safe_index = fc_indexInfo->ii_Expressions == NIL &&
		fc_indexInfo->ii_Predicate == NIL;

	/*
	 * 如果合适，则报告索引创建（检查大多数错误后再延迟此操作）。
	 */
	if (fc_stmt->isconstraint && !fc_quiet)
	{
		const char *fc_constraint_type;

		if (fc_stmt->primary)
			fc_constraint_type = "PRIMARY KEY";
		else if (fc_stmt->unique)
			fc_constraint_type = "UNIQUE";
		else if (fc_stmt->excludeOpNames != NIL)
			fc_constraint_type = "EXCLUDE";
		else
		{
			elog(ERROR, "unknown constraint type");
			fc_constraint_type = NULL; /* 保持编译器安静 */
		}

		ereport(DEBUG1,
				(errmsg_internal("%s %s will create implicit index \"%s\" for table \"%s\"",
								 fc_is_alter_table ? "ALTER TABLE / ADD" : "CREATE TABLE /",
								 fc_constraint_type,
								 fc_indexRelationName, RelationGetRelationName(fc_rel))));
	}

	/*
	 * 有效的 stmt->oldNode 意味着我们已经构建了索引的形式。
	 * 调用者也应该拒绝任何索引构建。
	 */
	Assert(!OidIsValid(fc_stmt->oldNode) || (fc_skip_build && !fc_concurrent));

	/*
	 * 为索引创建目录条目，包括约束。此步骤实际上也会构建索引，
	 * 除非调用者请求不这样做或者在并发模式下，在这种情况下将稍后完成，
	 * 或者正在执行分区索引（因为它们没有存储）。
	 */
	fc_flags = fc_constr_flags = 0;
	if (fc_stmt->isconstraint)
		fc_flags |= INDEX_CREATE_ADD_CONSTRAINT;
	if (fc_skip_build || fc_concurrent || fc_partitioned)
		fc_flags |= INDEX_CREATE_SKIP_BUILD;
	if (fc_stmt->if_not_exists)
		fc_flags |= INDEX_CREATE_IF_NOT_EXISTS;
	if (fc_concurrent)
		fc_flags |= INDEX_CREATE_CONCURRENT;
	if (fc_partitioned)
		fc_flags |= INDEX_CREATE_PARTITIONED;
	if (fc_stmt->primary)
		fc_flags |= INDEX_CREATE_IS_PRIMARY;

	/*
	 * 如果表是分区的，并且递归被拒绝但存在分区，则将索引标记为无效。
	 */
	if (fc_partitioned && fc_stmt->relation && !fc_stmt->relation->inh)
	{
		PartitionDesc fc_pd = RelationGetPartitionDesc(fc_rel, true);

		if (fc_pd->nparts != 0)
			fc_flags |= INDEX_CREATE_INVALID;
	}

	if (fc_stmt->deferrable)
		fc_constr_flags |= INDEX_CONSTR_CREATE_DEFERRABLE;
	if (fc_stmt->initdeferred)
		fc_constr_flags |= INDEX_CONSTR_CREATE_INIT_DEFERRED;

	fc_indexRelationId =
		index_create(fc_rel, fc_indexRelationName, fc_indexRelationId, fc_parentIndexId,
					 fc_parentConstraintId,
					 fc_stmt->oldNode, fc_indexInfo, fc_indexColNames,
					 fc_accessMethodId, fc_tablespaceId,
					 fc_collationObjectId, fc_classObjectId,
					 fc_coloptions, fc_reloptions,
					 fc_flags, fc_constr_flags,
					 allowSystemTableMods, !fc_check_rights,
					 &fc_createdConstraintId);

	ObjectAddressSet(fc_address, RelationRelationId, fc_indexRelationId);

	if (!OidIsValid(fc_indexRelationId))
	{
		/*
		 * 回滚通过索引函数执行的任何 GUC 更改。
		 * 如果我们上面更改了原始 default_tablespace，也要恢复。
		 */
		AtEOXact_GUC(false, fc_root_save_nestlevel);

		/* 恢复 userid 和安全上下文 */
		SetUserIdAndSecContext(fc_root_save_userid, fc_root_save_sec_context);

		table_close(fc_rel, NoLock);

		/* 如果这是顶层索引，我们完成了 */
		if (!OidIsValid(fc_parentIndexId))
			pgstat_progress_end_command();

		return fc_address;
	}

	/*
	 * 回滚通过索引函数执行的任何 GUC 更改，并保持
	 * 随后的更改局限于此命令。如果某些
	 * 索引函数更改了影响行为的 GUC，例如 search_path，
	 * 这非常重要。
	 */
	AtEOXact_GUC(false, fc_root_save_nestlevel);
	fc_root_save_nestlevel = NewGUCNestLevel();

	/* 添加任何请求的注释 */
	if (fc_stmt->idxcomment != NULL)
		CreateComments(fc_indexRelationId, RelationRelationId, 0,
					   fc_stmt->idxcomment);

	if (fc_partitioned)
	{
		PartitionDesc fc_partdesc;

		/*
		 * 除非调用者指定跳过此步骤（通过 ONLY），否则处理每个
		 * 分区，以确保它们都包含相应的索引。
		 *
		 * 如果我们是内部调用（没有 stmt->relation），则总是递归。
		 */
		fc_partdesc = RelationGetPartitionDesc(fc_rel, true);
		if ((!fc_stmt->relation || fc_stmt->relation->inh) && fc_partdesc->nparts > 0)
		{
			int			fc_nparts = fc_partdesc->nparts;
			Oid		   *fc_part_oids = palloc(sizeof(Oid) * fc_nparts);
			bool		fc_invalidate_parent = false;
			Relation	fc_parentIndex;
			TupleDesc	fc_parentDesc;

			pgstat_progress_update_param(PROGRESS_CREATEIDX_PARTITIONS_TOTAL,
										 fc_nparts);

			/* 为安全起见，制作 partdesc->oids[] 的本地副本 */
			memcpy(fc_part_oids, fc_partdesc->oids, sizeof(Oid) * fc_nparts);

			/*
			 * 我们需要一个描述父索引的 IndexInfo。上面构建的
			 * 几乎足够好，但不完全，因为（例如）它的谓词表达式
			 * 如果有的话还没有经过表达式预处理。
			 * 获取一个与子索引匹配的 IndexInfo 最可靠的
			 * 方法是通过 BuildIndexInfo() 以同样的方式构建它。
			 */
			fc_parentIndex = index_open(fc_indexRelationId, fc_lockmode);
			fc_indexInfo = BuildIndexInfo(fc_parentIndex);

			fc_parentDesc = RelationGetDescr(fc_rel);

			/*
			 * 对于每个分区，扫描所有现有索引；如果一个与
			 * 我们的索引定义匹配且尚未附加到其他
			 * 父索引，便将其附加到我们刚创建的一个。
			 *
			 * 如果没有匹配项，调用自己递归构建一个新索引，
			 * 使用相同的选项（索引名称除外）。
			 */
			for (fc_i = 0; fc_i < fc_nparts; fc_i++)
			{
				Oid			fc_childRelid = fc_part_oids[fc_i];
				Relation	fc_childrel;
				Oid			fc_child_save_userid;
				int			fc_child_save_sec_context;
				int			fc_child_save_nestlevel;
				List	   *fc_childidxs;
				ListCell   *fc_cell;
				AttrMap    *fc_attmap;
				bool		fc_found = false;

				fc_childrel = table_open(fc_childRelid, fc_lockmode);

				GetUserIdAndSecContext(&fc_child_save_userid,
									   &fc_child_save_sec_context);
				SetUserIdAndSecContext(fc_childrel->rd_rel->relowner,
									   fc_child_save_sec_context | SECURITY_RESTRICTED_OPERATION);
				fc_child_save_nestlevel = NewGUCNestLevel();

				/*
				 * 不要尝试在外部表上创建索引。如果是常规索引则跳过这些，
				 * 如果尝试创建约束索引则失败。
				 */
				if (fc_childrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
				{
					if (fc_stmt->unique || fc_stmt->primary)
						ereport(ERROR,
								(errcode(ERRCODE_WRONG_OBJECT_TYPE),
								 errmsg("cannot create unique index on partitioned table \"%s\"",
										RelationGetRelationName(fc_rel)),
								 errdetail("Table \"%s\" contains partitions that are foreign tables.",
										   RelationGetRelationName(fc_rel))));

					AtEOXact_GUC(false, fc_child_save_nestlevel);
					SetUserIdAndSecContext(fc_child_save_userid,
										   fc_child_save_sec_context);
					table_close(fc_childrel, fc_lockmode);
					continue;
				}

				fc_childidxs = RelationGetIndexList(fc_childrel);
				fc_attmap =
					build_attrmap_by_name(RelationGetDescr(fc_childrel),
										  fc_parentDesc);

				foreach(fc_cell, fc_childidxs)
				{
					Oid			fc_cldidxid = lfirst_oid(fc_cell);
					Relation	fc_cldidx;
					IndexInfo  *fc_cldIdxInfo;

					/* 此索引已经是另一个的分区 */
					if (has_superclass(fc_cldidxid))
						continue;

					fc_cldidx = index_open(fc_cldidxid, fc_lockmode);
					fc_cldIdxInfo = BuildIndexInfo(fc_cldidx);
					if (CompareIndexInfo(fc_cldIdxInfo, fc_indexInfo,
										 fc_cldidx->rd_indcollation,
										 fc_parentIndex->rd_indcollation,
										 fc_cldidx->rd_opfamily,
										 fc_parentIndex->rd_opfamily,
										 fc_attmap))
					{
						Oid			fc_cldConstrOid = InvalidOid;

						/*
						 * 找到匹配。
						 *
						 * 如果由于约束在父级中创建此索引，
						 * 则子级也需要有一个约束，因此寻找一个。
						 * 如果没有这样的约束，则此索引无效，因此
						 * 继续查找。
						 */
						if (fc_createdConstraintId != InvalidOid)
						{
							fc_cldConstrOid =
								get_relation_idx_constraint_oid(fc_childRelid,
																fc_cldidxid);
							if (fc_cldConstrOid == InvalidOid)
							{
								index_close(fc_cldidx, fc_lockmode);
								continue;
							}
						}

						/* 将索引附加到父级，我们完成了。 */
						IndexSetParentIndex(fc_cldidx, fc_indexRelationId);
						if (fc_createdConstraintId != InvalidOid)
							ConstraintSetParentConstraint(fc_cldConstrOid,
														  fc_createdConstraintId,
														  fc_childRelid);

						if (!fc_cldidx->rd_index->indisvalid)
							fc_invalidate_parent = true;

						fc_found = true;
						/* 保持锁定直到提交 */
						index_close(fc_cldidx, NoLock);
						break;
					}

					index_close(fc_cldidx, fc_lockmode);
				}

				list_free(fc_childidxs);
				AtEOXact_GUC(false, fc_child_save_nestlevel);
				SetUserIdAndSecContext(fc_child_save_userid,
									   fc_child_save_sec_context);
				table_close(fc_childrel, NoLock);

				/*
				 * 如果没有找到匹配的索引，则创建我们自己的。
				 */
				if (!fc_found)
				{
					IndexStmt  *fc_childStmt = copyObject(fc_stmt);
					bool		fc_found_whole_row;
					ListCell   *fc_lc;
					ObjectAddress fc_childAddr;

					/*
					 * 我们无法为子索引使用相同的索引名称，
					 * 因此清除 idxname 以便递归调用选择
					 * 新名称。同样，现有的目标关系
					 * 字段是错误的，如果 indexOid 或 oldNode 被设置，
					 * 它们也不能应用于子索引。
					 */
					fc_childStmt->idxname = NULL;
					fc_childStmt->relation = NULL;
					fc_childStmt->indexOid = InvalidOid;
					fc_childStmt->oldNode = InvalidOid;
					fc_childStmt->oldCreateSubid = InvalidSubTransactionId;
					fc_childStmt->oldFirstRelfilenodeSubid = InvalidSubTransactionId;

					/*
					 * 调整任何 Vars（在表达式和索引的
					 * WHERE 子句中）以匹配分区的列编号，
					 * 以防它与父级不同。
					 */
					foreach(fc_lc, fc_childStmt->indexParams)
					{
						IndexElem  *fc_ielem = lfirst(fc_lc);

						/*
						 * 如果索引参数是一个表达式，我们必须
						 * 将其转换为包含子 Vars。
						 */
						if (fc_ielem->expr)
						{
							fc_ielem->expr =
								map_variable_attnos((Node *) fc_ielem->expr,
													1, 0, fc_attmap,
													InvalidOid,
													&fc_found_whole_row);
							if (fc_found_whole_row)
								elog(ERROR, "cannot convert whole-row table reference");
						}
					}
					fc_childStmt->whereClause =
						map_variable_attnos(fc_stmt->whereClause, 1, 0,
											fc_attmap,
											InvalidOid, &fc_found_whole_row);
					if (fc_found_whole_row)
						elog(ERROR, "cannot convert whole-row table reference");

					/*
					 * 以起始用户 ID 递归。被调用者将使用该
					 * ID 进行权限检查，然后再次切换。
					 */
					Assert(GetUserId() == fc_child_save_userid);
					SetUserIdAndSecContext(fc_root_save_userid,
										   fc_root_save_sec_context);
					fc_childAddr =
						DefineIndex(fc_childRelid, fc_childStmt,
									InvalidOid, /* 没有预定义的 OID */
									fc_indexRelationId,	/* 这是我们的子项 */
									fc_createdConstraintId,
									fc_is_alter_table, fc_check_rights,
									fc_check_not_in_use,
									fc_skip_build, fc_quiet);
					SetUserIdAndSecContext(fc_child_save_userid,
										   fc_child_save_sec_context);

					/*
					 * 检查刚创建的索引是否有效，因为它
					 * 可能在跨多个分区层级递归时被标记为无效。
					 */
					if (!get_index_isvalid(fc_childAddr.objectId))
						fc_invalidate_parent = true;
				}

				pgstat_progress_update_param(PROGRESS_CREATEIDX_PARTITIONS_DONE,
											 fc_i + 1);
				free_attrmap(fc_attmap);
			}

			index_close(fc_parentIndex, fc_lockmode);

			/*
			 * 我们为这个索引插入的 pg_index 行被标记
			 * indisvalid=true。但是如果我们附加一个无效的现有索引，这就是不正确的，因此也要将我们的行更新为无效。
			 */
			if (fc_invalidate_parent)
			{
				Relation	fc_pg_index = table_open(IndexRelationId, RowExclusiveLock);
				HeapTuple	fc_tup,
							fc_newtup;

				fc_tup = SearchSysCache1(INDEXRELID,
									  ObjectIdGetDatum(fc_indexRelationId));
				if (!HeapTupleIsValid(fc_tup))
					elog(ERROR, "cache lookup failed for index %u",
						 fc_indexRelationId);
				fc_newtup = heap_copytuple(fc_tup);
				((Form_pg_index) GETSTRUCT(fc_newtup))->indisvalid = false;
				CatalogTupleUpdate(fc_pg_index, &fc_tup->t_self, fc_newtup);
				ReleaseSysCache(fc_tup);
				table_close(fc_pg_index, RowExclusiveLock);
				heap_freetuple(fc_newtup);

				/*
				 * 这里进行 CCI 更新以使该更新可见，以防这在跨越多个分区层级时递归。
				 */
				CommandCounterIncrement();
			}
		}

		/*
		 * 在分区表上的索引本身不会被构建，因此我们
		 * 在这里完成了。
		 */
		AtEOXact_GUC(false, fc_root_save_nestlevel);
		SetUserIdAndSecContext(fc_root_save_userid, fc_root_save_sec_context);
		table_close(fc_rel, NoLock);
		if (!OidIsValid(fc_parentIndexId))
			pgstat_progress_end_command();
		return fc_address;
	}

	AtEOXact_GUC(false, fc_root_save_nestlevel);
	SetUserIdAndSecContext(fc_root_save_userid, fc_root_save_sec_context);

	if (!fc_concurrent)
	{
		/* 关闭堆，我们完成了，非并发情况下 */
		table_close(fc_rel, NoLock);

		/* 如果这是顶层索引，我们完成了。 */
		if (!OidIsValid(fc_parentIndexId))
			pgstat_progress_end_command();

		return fc_address;
	}

	/* 保存 lockrelid 和 locktag 以供后用，然后关闭关系 */
	fc_heaprelid = fc_rel->rd_lockInfo.lockRelId;
	SET_LOCKTAG_RELATION(fc_heaplocktag, fc_heaprelid.dbId, fc_heaprelid.relId);
	table_close(fc_rel, NoLock);

	/*
	 * 对于并发构建，在我们开始构建索引之前
	 * 使目录条目对其他事务可见是重要的。那
	 * 将防止它们进行不兼容的 HOT 更新。新的索引
	 * 将被标记为不 indisready 和不 indisvalid，以便没有人
	 * 尝试向其插入或将其用于查询。
	 *
	 * 我们必须提交当前事务，以使索引变得
	 * 可见；然后开始另一个。请注意，我们刚刚
	 * 构建的所有数据结构在提交时都会丢失。我们所保留的
	 * 唯一数据是关系 ID。
	 *
	 * 在提交之前，对表格进行会话级锁定，以确保
	 * 在我们完成之前不能删除它或索引。这
	 * 不能被阻塞，即使其他人在等待访问，因为我们
	 * 在事务中已经持有相同的锁。
	 *
	 * 注意：我们目前不对索引使用会话锁，
	 * 因为在我们持锁父表时，没有任何可能改变其状态的操作。
	 * 这可能需要稍后更改。
	 */
	LockRelationIdForSession(&fc_heaprelid, ShareUpdateExclusiveLock);

	PopActiveSnapshot();
	CommitTransactionCommand();
	StartTransactionCommand();

	/* 告诉并发索引构建忽略我们，如果索引合格 */
	if (fc_safe_index)
		fc_set_indexsafe_procflags();

	/*
	 * 现在索引是可见的，因此我们可以报告OID。在此期间，
	 * 包括第2阶段开始时的报告。
	 */
	{
		const int	fc_progress_cols[] = {
			PROGRESS_CREATEIDX_INDEX_OID,
			PROGRESS_CREATEIDX_PHASE
		};
		const int64 fc_progress_vals[] = {
			fc_indexRelationId,
			PROGRESS_CREATEIDX_PHASE_WAIT_1
		};

		pgstat_progress_update_multi_param(2, fc_progress_cols, fc_progress_vals);
	}

	/*
	 * 并发索引构建的第2阶段（有关如何工作的概述，请参见validate_index()的注释）
	 *
	 * 现在我们必须等待，直到没有正在运行的事务能够以旧的索引列表打开表。
	 * 使用ShareLock来考虑持有允许写表锁的正在运行的事务。请注意，我们
	 * 不需要担心在此时之后打开表进行写入的事务；它们在打开时会看到新的索引。
	 *
	 * 注意：我们在这里使用实际锁定获取的原因，而不仅仅是检查
	 * ProcArray并休眠，是因为如果其中一个相关事务在试图获取
	 * 我们表的独占锁时被阻塞，则可能会发生死锁。锁定代码会检测死锁并
	 * 正确处理错误。
	 */
	WaitForLockers(fc_heaplocktag, ShareLock, true);

	/*
	 * 此时我们确定没有事务以写入方式打开表且
	 * 其索引列表中没有此新索引。我们已经等待
	 * 所有现有事务，任何新事务都将具有新索引在其列表中，
	 * 但索引仍然标记为“未准备好插入”。虽然在
	 * 决定HOT安全性时会咨询索引。这个安排确保不会
	 * 创建新的HOT链，其中新元组和链中的旧元组
	 * 具有不同的索引键。
	 *
	 * 我们现在拍摄一个新的快照，并使用此快照中
	 * 可见的所有元组构建索引。我们可以确信对这些元组的任何
	 * HOT更新将与索引兼容，因为未
	 * 知道索引的事务所做的任何更新现在都已提交或回滚。
	 * 因此，每个可见元组或是其HOT链的结束，或链的扩展
	 * 对于此索引是HOT安全的。
	 */

	/* 设置ActiveSnapshot，因为索引中的函数可能需要它 */
	PushActiveSnapshot(GetTransactionSnapshot());

	/* 执行索引的并发构建 */
	index_concurrently_build(fc_relationId, fc_indexRelationId);

	/* 我们可以摆脱我们的快照 */
	PopActiveSnapshot();

	/*
	 * 提交此事务以使不可准备的更新可见。
	 */
	CommitTransactionCommand();
	StartTransactionCommand();

	/* 告诉并发索引构建忽略我们，如果索引合格 */
	if (fc_safe_index)
		fc_set_indexsafe_procflags();

	/*
	 * 并发索引构建的第3阶段
	 *
	 * 我们再次等待，直到没有事务能够以只读模式打开表
	 * 的索引进行更新。
	 */
	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
								 PROGRESS_CREATEIDX_PHASE_WAIT_2);
	WaitForLockers(fc_heaplocktag, ShareLock, true);

	/*
	 * 现在获取将由 validate_index() 用于过滤候选元组的“参考快照”。注意！可能仍然有快照在使用中，它们将某些事务视为进行中，而我们的参考快照则将其视为已提交。如果这样的最近提交的事务删除了表中的元组，我们将不会将它们包含在索引中；然而，那些将删除事务视为仍在进行中的事务将预计在我们将索引标记为有效后，这些元组会存在。
	 *
	 * 我们通过等待所有处于危险中的事务退出来解决这个问题，然后再将索引标记为有效。
	 *
	 * 我们还将 ActiveSnapshot 设置为这个快照，因为索引中的函数可能需要一个快照。
	 */
	fc_snapshot = RegisterSnapshot(GetTransactionSnapshot());
	PushActiveSnapshot(fc_snapshot);

	/*
	 * 扫描索引和堆，插入任何缺失的索引条目。
	 */
	validate_index(fc_relationId, fc_indexRelationId, fc_snapshot);

	/*
	 * 删除参考快照。我们必须在等待其他快照持有者之前执行此操作，否则我们将与其他也在执行 CREATE INDEX CONCURRENTLY 的过程发生死锁，这些过程将我们的快照视为必须等待的快照。但首先，保存快照的 xmin 以用作 GetCurrentVirtualXIDs() 的 limitXmin。
	 */
	fc_limitXmin = fc_snapshot->xmin;

	PopActiveSnapshot();
	UnregisterSnapshot(fc_snapshot);

	/*
	 * 快照子系统仍然可能包含注册的快照，这些快照正在阻碍我们进程的广告 xmin；特别是，如果 default_transaction_isolation = serializable，则有一个仍然活跃的事务快照。CatalogSnapshot 同样也是一个危险。为了确保没有死锁，我们必须提交并启动另一个事务，并在任何快照被拍摄之前进行等待。
	 */
	CommitTransactionCommand();
	StartTransactionCommand();

	/* 告诉并发索引构建忽略我们，如果索引合格 */
	if (fc_safe_index)
		fc_set_indexsafe_procflags();

	/* 现在我们绝对不应该宣传任何 xmin。 */
	Assert(MyProc->xmin == InvalidTransactionId);

	/*
	 * 现在索引是有效的，因为它包含所有当前有趣的元组。但由于它可能不包含在获取参考快照之前刚刚删除的元组，我们必须等待任何可能有较旧快照的事务。
	 */
	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
								 PROGRESS_CREATEIDX_PHASE_WAIT_3);
	WaitForOlderSnapshots(fc_limitXmin, true);

	/*
	 * 索引现在可以标记为有效 -- 更新其 pg_index 条目
	 */
	index_set_state_flags(fc_indexRelationId, INDEX_CREATE_SET_VALID);

	/*
	 * pg_index 更新将导致后端（包括此后端）更新索引本身的 relcache 条目，但我们还应该对父表发送 relcache 无效，以强制重新规划缓存计划。否则现有会话可能无法在有用的地方使用新的索引。（注意，我们之前的提交并没有产生重新规划的理由；因此，仅对索引本身进行 relcache 刷新就足够了。）
	 */
	CacheInvalidateRelcacheByRelid(fc_heaprelid.relId);

	/*
	 * 最后要做的是释放对父表的会话级锁。
	 */
	UnlockRelationIdForSession(&fc_heaprelid, ShareUpdateExclusiveLock);

	pgstat_progress_end_command();

	return fc_address;
}


/*
 * CheckPredicate
 *		检查给定的部分索引谓词是否有效。
 *
 * 这曾经还限制谓词的形式为 indxpath.c 可以处理的形式。然而，这似乎过于
 * 限制。部分索引的一个有用应用是在表的子集上应用唯一约束，在这种情况下
 * 任何可评估的谓词都适用。因此这里接受任何谓词
 * （除了需要计划的谓词），让 indxpath.c 自行处理。
 */
static void fc_CheckPredicate(Expr *fc_predicate)
{
	/*
	 * transformExpr() 应该已经根据谓词的 EXPR_KIND_ 拒绝了子查询、聚合
	 * 和窗口函数。
	 */

	/*
	 * 使用可变函数的谓词可能是错误的，因为我们不允许索引表达式使用它。
	 */
	if (contain_mutable_functions_after_planning(fc_predicate))
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
				 errmsg("functions in index predicate must be marked IMMUTABLE")));
}

/*
 * 计算每个索引列的信息，包括索引的列号
 * 或索引表达式、操作类别及其选项。请注意，所有输出向量
 * 应为所有列分配，包括“包含”的列。
 *
 * 如果调用者切换到表的所有者，ddl_userid 是无需遍历不透明表达式而到达的 ACL
 * 检查的角色。否则，它是 InvalidOid，其他 ddl_* 参数未定义。
 */
static void fc_ComputeIndexAttrs(IndexInfo *fc_indexInfo,
				  Oid *fc_typeOidP,
				  Oid *fc_collationOidP,
				  Oid *fc_classOidP,
				  int16 *fc_colOptionP,
				  List *fc_attList,	/* IndexElem 的列表 */
				  List *fc_exclusionOpNames,
				  Oid fc_relId,
				  const char *fc_accessMethodName,
				  Oid fc_accessMethodId,
				  bool fc_amcanorder,
				  bool fc_isconstraint,
				  Oid fc_ddl_userid,
				  int fc_ddl_sec_context,
				  int *fc_ddl_save_nestlevel)
{
	ListCell   *fc_nextExclOp;
	ListCell   *fc_lc;
	int			fc_attn;
	int			fc_nkeycols = fc_indexInfo->ii_NumIndexKeyAttrs;
	Oid			fc_save_userid;
	int			fc_save_sec_context;

	/* 如果需要，为排除操作符信息分配空间 */
	if (fc_exclusionOpNames)
	{
		Assert(list_length(fc_exclusionOpNames) == fc_nkeycols);
		fc_indexInfo->ii_ExclusionOps = (Oid *) palloc(sizeof(Oid) * fc_nkeycols);
		fc_indexInfo->ii_ExclusionProcs = (Oid *) palloc(sizeof(Oid) * fc_nkeycols);
		fc_indexInfo->ii_ExclusionStrats = (uint16 *) palloc(sizeof(uint16) * fc_nkeycols);
		fc_nextExclOp = list_head(fc_exclusionOpNames);
	}
	else
		fc_nextExclOp = NULL;

	if (OidIsValid(fc_ddl_userid))
		GetUserIdAndSecContext(&fc_save_userid, &fc_save_sec_context);

	/*
	 * 处理属性列表
	 */
	fc_attn = 0;
	foreach(fc_lc, fc_attList)
	{
		IndexElem  *fc_attribute = (IndexElem *) lfirst(fc_lc);
		Oid			fc_atttype;
		Oid			fc_attcollation;

		/*
		 * 处理要索引的列或表达式。
		 */
		if (fc_attribute->name != NULL)
		{
			/* 简单索引属性 */
			HeapTuple	fc_atttuple;
			Form_pg_attribute fc_attform;

			Assert(fc_attribute->expr == NULL);
			fc_atttuple = SearchSysCacheAttName(fc_relId, fc_attribute->name);
			if (!HeapTupleIsValid(fc_atttuple))
			{
				/* 错误信息拼写的差异是历史原因 */
				if (fc_isconstraint)
					ereport(ERROR,
							(errcode(ERRCODE_UNDEFINED_COLUMN),
							 errmsg("column \"%s\" named in key does not exist",
									fc_attribute->name)));
				else
					ereport(ERROR,
							(errcode(ERRCODE_UNDEFINED_COLUMN),
							 errmsg("column \"%s\" does not exist",
									fc_attribute->name)));
			}
			fc_attform = (Form_pg_attribute) GETSTRUCT(fc_atttuple);
			fc_indexInfo->ii_IndexAttrNumbers[fc_attn] = fc_attform->attnum;
			fc_atttype = fc_attform->atttypid;
			fc_attcollation = fc_attform->attcollation;
			ReleaseSysCache(fc_atttuple);
		}
		else
		{
			/* 索引表达式 */
			Node	   *fc_expr = fc_attribute->expr;

			Assert(fc_expr != NULL);

			if (fc_attn >= fc_nkeycols)
				ereport(ERROR,
						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
						 errmsg("expressions are not supported in included columns")));
			fc_atttype = exprType(fc_expr);
			fc_attcollation = exprCollation(fc_expr);

			/*
			 * 删除任何顶层的 COLLATE 子句。这确保我们将
			 * "x COLLATE y" 和 "(x COLLATE y)" 视为相同。
			 */
			while (IsA(fc_expr, CollateExpr))
				fc_expr = (Node *) ((CollateExpr *) fc_expr)->arg;

			if (IsA(fc_expr, Var) &&
				((Var *) fc_expr)->varattno != InvalidAttrNumber)
			{
				/*
				 * 用户写了 "(column)" 或 "(column COLLATE something)"。
				 * 无论如何都将其视为简单属性。
				 */
				fc_indexInfo->ii_IndexAttrNumbers[fc_attn] = ((Var *) fc_expr)->varattno;
			}
			else
			{
				fc_indexInfo->ii_IndexAttrNumbers[fc_attn] = 0;	/* 标记表达式 */
				fc_indexInfo->ii_Expressions = lappend(fc_indexInfo->ii_Expressions,
													fc_expr);

				/*
				 * transformExpr() 应该已经根据索引表达式的 EXPR_KIND_
				 * 拒绝了子查询、聚合和窗口函数。
				 */

				/*
				 * 使用可变函数的表达式可能是错误的，因为如果你每次对
				 * 相同的数据得不到相同的结果，那么索引条目的意义就不清晰。
				 */
				if (contain_mutable_functions_after_planning((Expr *) fc_expr))
					ereport(ERROR,
							(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
							 errmsg("functions in index expression must be marked IMMUTABLE")));
			}
		}

		fc_typeOidP[fc_attn] = fc_atttype;

		/*
		 * 包含的列没有排序规则，没有操作类，也没有排序
		 * 选项。
		 */
		if (fc_attn >= fc_nkeycols)
		{
			if (fc_attribute->collation)
				ereport(ERROR,
						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
						 errmsg("including column does not support a collation")));
			if (fc_attribute->opclass)
				ereport(ERROR,
						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
						 errmsg("including column does not support an operator class")));
			if (fc_attribute->ordering != SORTBY_DEFAULT)
				ereport(ERROR,
						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
						 errmsg("including column does not support ASC/DESC options")));
			if (fc_attribute->nulls_ordering != SORTBY_NULLS_DEFAULT)
				ereport(ERROR,
						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
						 errmsg("including column does not support NULLS FIRST/LAST options")));

			fc_classOidP[fc_attn] = InvalidOid;
			fc_colOptionP[fc_attn] = 0;
			fc_collationOidP[fc_attn] = InvalidOid;
			fc_attn++;

			continue;
		}

		/*
		 * 如果有的话，应用排序规则覆盖。使用 ddl_userid 是必要的
		 * 因为其中的 ACL 检查，而且这是安全的，因为排序规则不
		 * 包含不透明表达式（或非不透明表达式）。
		 */
		if (fc_attribute->collation)
		{
			if (OidIsValid(fc_ddl_userid))
			{
				AtEOXact_GUC(false, *fc_ddl_save_nestlevel);
				SetUserIdAndSecContext(fc_ddl_userid, fc_ddl_sec_context);
			}
			fc_attcollation = get_collation_oid(fc_attribute->collation, false);
			if (OidIsValid(fc_ddl_userid))
			{
				SetUserIdAndSecContext(fc_save_userid, fc_save_sec_context);
				*fc_ddl_save_nestlevel = NewGUCNestLevel();
			}
		}

		/*
		 * 如果这是可排序的类型，则检查我们是否有排序规则。这里唯一
		 * 预期的失败是（1）应用于不可排序类型的 COLLATE，或（2）索引
		 * 表达式具有未解析的排序规则。但我们可以编写代码进行完整的一致性检查。
		 */
		if (type_is_collatable(fc_atttype))
		{
			if (!OidIsValid(fc_attcollation))
				ereport(ERROR,
						(errcode(ERRCODE_INDETERMINATE_COLLATION),
						 errmsg("could not determine which collation to use for index expression"),
						 errhint("Use the COLLATE clause to set the collation explicitly.")));
		}
		else
		{
			if (OidIsValid(fc_attcollation))
				ereport(ERROR,
						(errcode(ERRCODE_DATATYPE_MISMATCH),
						 errmsg("collations are not supported by type %s",
								format_type_be(fc_atttype))));
		}

		fc_collationOidP[fc_attn] = fc_attcollation;

		/*
		 * 确定要使用的操作类。由于其中的 ACL 检查，使用 ddl_userid 是必要的。
		 * 尽管操作类包含不透明表达式（具体来说是函数），但这仍然是安全的，
		 * 因为只有超级用户可以定义操作类。
		 */
		if (OidIsValid(fc_ddl_userid))
		{
			AtEOXact_GUC(false, *fc_ddl_save_nestlevel);
			SetUserIdAndSecContext(fc_ddl_userid, fc_ddl_sec_context);
		}
		fc_classOidP[fc_attn] = ResolveOpClass(fc_attribute->opclass,
										 fc_atttype,
										 fc_accessMethodName,
										 fc_accessMethodId);
		if (OidIsValid(fc_ddl_userid))
		{
			SetUserIdAndSecContext(fc_save_userid, fc_save_sec_context);
			*fc_ddl_save_nestlevel = NewGUCNestLevel();
		}

		/*
		 * 识别排除操作符（如果有）。
		 */
		if (fc_nextExclOp)
		{
			List	   *fc_opname = (List *) lfirst(fc_nextExclOp);
			Oid			fc_opid;
			Oid			fc_opfamily;
			int			fc_strat;

			/*
			 * 找到操作符——它必须接受列数据类型
			 * 而无需运行时强制转换（但二进制兼容是可以的）。
			 * 操作符包含不透明表达式（特别是函数）。
			 * compatible_oper_opid() 最终归结为 oper() 和
			 * IsBinaryCoercible()。如果 oper() 开始调用不透明表达式，
			 * PostgreSQL 将在其他地方出现安全问题。
			 */
			if (OidIsValid(fc_ddl_userid))
			{
				AtEOXact_GUC(false, *fc_ddl_save_nestlevel);
				SetUserIdAndSecContext(fc_ddl_userid, fc_ddl_sec_context);
			}
			fc_opid = compatible_oper_opid(fc_opname, fc_atttype, fc_atttype, false);
			if (OidIsValid(fc_ddl_userid))
			{
				SetUserIdAndSecContext(fc_save_userid, fc_save_sec_context);
				*fc_ddl_save_nestlevel = NewGUCNestLevel();
			}

			/*
			 * 仅允许使用交换律操作符在排除
			 * 约束中。如果 X 与 Y 冲突，但 Y 不与 X 冲突，
			 * 将会发生不良后果。
			 */
			if (get_commutator(fc_opid) != fc_opid)
				ereport(ERROR,
						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
						 errmsg("operator %s is not commutative",
								format_operator(fc_opid)),
						 errdetail("Only commutative operators can be used in exclusion constraints.")));

			/*
			 * 操作符也必须是正确 opfamily 的成员
			 */
			fc_opfamily = get_opclass_family(fc_classOidP[fc_attn]);
			fc_strat = get_op_opfamily_strategy(fc_opid, fc_opfamily);
			if (fc_strat == 0)
			{
				HeapTuple	fc_opftuple;
				Form_pg_opfamily fc_opfform;

				/*
				 * attribute->opclass 可能没有明确命名 opfamily，
				 * 因此获取所选 opfamily 的名称以用于
				 * 错误消息。
				 */
				fc_opftuple = SearchSysCache1(OPFAMILYOID,
										   ObjectIdGetDatum(fc_opfamily));
				if (!HeapTupleIsValid(fc_opftuple))
					elog(ERROR, "cache lookup failed for opfamily %u",
						 fc_opfamily);
				fc_opfform = (Form_pg_opfamily) GETSTRUCT(fc_opftuple);

				ereport(ERROR,
						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
						 errmsg("operator %s is not a member of operator family \"%s\"",
								format_operator(fc_opid),
								NameStr(fc_opfform->opfname)),
						 errdetail("The exclusion operator must be related to the index operator class for the constraint.")));
			}

			fc_indexInfo->ii_ExclusionOps[fc_attn] = fc_opid;
			fc_indexInfo->ii_ExclusionProcs[fc_attn] = get_opcode(fc_opid);
			fc_indexInfo->ii_ExclusionStrats[fc_attn] = fc_strat;
			fc_nextExclOp = lnext(fc_exclusionOpNames, fc_nextExclOp);
		}

		/*
		 * 设置每列选项（indoption 字段）。目前，对于任何无序索引，
		 * 这为零，而有序索引则有 DESC 和
		 * NULLS FIRST/LAST 选项。
		 */
		fc_colOptionP[fc_attn] = 0;
		if (fc_amcanorder)
		{
			/* 默认排序为 ASC */
			if (fc_attribute->ordering == SORTBY_DESC)
				fc_colOptionP[fc_attn] |= INDOPTION_DESC;
			/* 默认空值排序为 ASC 时为 LAST，DESC 时为 FIRST */
			if (fc_attribute->nulls_ordering == SORTBY_NULLS_DEFAULT)
			{
				if (fc_attribute->ordering == SORTBY_DESC)
					fc_colOptionP[fc_attn] |= INDOPTION_NULLS_FIRST;
			}
			else if (fc_attribute->nulls_ordering == SORTBY_NULLS_FIRST)
				fc_colOptionP[fc_attn] |= INDOPTION_NULLS_FIRST;
		}
		else
		{
			/* 索引 AM 不支持排序 */
			if (fc_attribute->ordering != SORTBY_DEFAULT)
				ereport(ERROR,
						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
						 errmsg("access method \"%s\" does not support ASC/DESC options",
								fc_accessMethodName)));
			if (fc_attribute->nulls_ordering != SORTBY_NULLS_DEFAULT)
				ereport(ERROR,
						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
						 errmsg("access method \"%s\" does not support NULLS FIRST/LAST options",
								fc_accessMethodName)));
		}

		/* 设置每列的 opclass 选项（attoptions 字段）。 */
		if (fc_attribute->opclassopts)
		{
			Assert(fc_attn < fc_nkeycols);

			if (!fc_indexInfo->ii_OpclassOptions)
				fc_indexInfo->ii_OpclassOptions =
					palloc0(sizeof(Datum) * fc_indexInfo->ii_NumIndexAttrs);

			fc_indexInfo->ii_OpclassOptions[fc_attn] =
				transformRelOptions((Datum) 0, fc_attribute->opclassopts,
									NULL, NULL, false, false);
		}

		fc_attn++;
	}
}

/*
 * 解析可能默认的操作符类规范
 *
 * 注意：这用于解析索引和
 * 分区键定义中的操作符类规范。
 */
Oid ResolveOpClass(List *fc_opclass, Oid fc_attrType,
			   const char *fc_accessMethodName, Oid fc_accessMethodId)
{
	char	   *fc_schemaname;
	char	   *fc_opcname;
	HeapTuple	fc_tuple;
	Form_pg_opclass fc_opform;
	Oid			fc_opClassId,
				fc_opInputType;

	if (fc_opclass == NIL)
	{
		/* 未指定操作符类，因此查找默认值 */
		fc_opClassId = GetDefaultOpClass(fc_attrType, fc_accessMethodId);
		if (!OidIsValid(fc_opClassId))
			ereport(ERROR,
					(errcode(ERRCODE_UNDEFINED_OBJECT),
					 errmsg("data type %s has no default operator class for access method \"%s\"",
							format_type_be(fc_attrType), fc_accessMethodName),
					 errhint("You must specify an operator class for the index or define a default operator class for the data type.")));
		return fc_opClassId;
	}

	/*
	 * 给定具体的 opclass 名称，因此查找相应的 opclass。
	 */

	/* 解构名称列表 */
	DeconstructQualifiedName(fc_opclass, &fc_schemaname, &fc_opcname);

	if (fc_schemaname)
	{
		/* 仅在特定模式中查找 */
		Oid			fc_namespaceId;

		fc_namespaceId = LookupExplicitNamespace(fc_schemaname, false);
		fc_tuple = SearchSysCache3(CLAAMNAMENSP,
								ObjectIdGetDatum(fc_accessMethodId),
								PointerGetDatum(fc_opcname),
								ObjectIdGetDatum(fc_namespaceId));
	}
	else
	{
		/* 未限定的 opclass 名称，因此搜索搜索路径 */
		fc_opClassId = OpclassnameGetOpcid(fc_accessMethodId, fc_opcname);
		if (!OidIsValid(fc_opClassId))
			ereport(ERROR,
					(errcode(ERRCODE_UNDEFINED_OBJECT),
					 errmsg("operator class \"%s\" does not exist for access method \"%s\"",
							fc_opcname, fc_accessMethodName)));
		fc_tuple = SearchSysCache1(CLAOID, ObjectIdGetDatum(fc_opClassId));
	}

	if (!HeapTupleIsValid(fc_tuple))
		ereport(ERROR,
				(errcode(ERRCODE_UNDEFINED_OBJECT),
				 errmsg("operator class \"%s\" does not exist for access method \"%s\"",
						NameListToString(fc_opclass), fc_accessMethodName)));

	/*
	 * 验证索引操作符类是否接受此数据类型。注意我们
	 * 将接受二进制兼容性。
	 */
	fc_opform = (Form_pg_opclass) GETSTRUCT(fc_tuple);
	fc_opClassId = fc_opform->oid;
	fc_opInputType = fc_opform->opcintype;

	if (!IsBinaryCoercible(fc_attrType, fc_opInputType))
		ereport(ERROR,
				(errcode(ERRCODE_DATATYPE_MISMATCH),
				 errmsg("operator class \"%s\" does not accept data type %s",
						NameListToString(fc_opclass), format_type_be(fc_attrType))));

	ReleaseSysCache(fc_tuple);

	return fc_opClassId;
}

/*
 * GetDefaultOpClass
 *
 * 给定数据类型和访问方法的 OID，查找默认
 * 操作符类（如果有）。如果没有则返回 InvalidOid。
 */
Oid GetDefaultOpClass(Oid fc_type_id, Oid fc_am_id)
{
	Oid			fc_result = InvalidOid;
	int			fc_nexact = 0;
	int			fc_ncompatible = 0;
	int			fc_ncompatiblepreferred = 0;
	Relation	fc_rel;
	ScanKeyData fc_skey[1];
	SysScanDesc fc_scan;
	HeapTuple	fc_tup;
	TYPCATEGORY fc_tcategory;

	/* 如果是域，改为查看基础类型 */
	fc_type_id = getBaseType(fc_type_id);

	fc_tcategory = TypeCategory(fc_type_id);

	/*
	 * 我们扫描访问方法可用的所有 opclasses，
	 * 寻找一个标记为默认并匹配目标类型的操作符类
	 * （要么完全匹配，要么二进制兼容，但优先考虑完全匹配）。
	 *
	 * 我们可能会找到多个二进制兼容的匹配项。如果只有一个
	 * 是所需类型，则使用该项；否则我们失败，强制用户
	 * 指定他想要哪个。（所需类型的特例是一个
	 * 便捷方案：它与 text 和 bpchar 二进制兼容，因此
	 * 我们需要一个决胜者。）如果我们找到多个完全匹配，
	 * 那么某人向 pg_opclass 中放入了虚假的条目。
	 */
	fc_rel = table_open(OperatorClassRelationId, AccessShareLock);

	ScanKeyInit(&fc_skey[0],
				Anum_pg_opclass_opcmethod,
				BTEqualStrategyNumber, F_OIDEQ,
				ObjectIdGetDatum(fc_am_id));

	fc_scan = systable_beginscan(fc_rel, OpclassAmNameNspIndexId, true,
							  NULL, 1, fc_skey);

	while (HeapTupleIsValid(fc_tup = systable_getnext(fc_scan)))
	{
		Form_pg_opclass fc_opclass = (Form_pg_opclass) GETSTRUCT(fc_tup);

		/* 如果不是默认 opclass，则完全忽略 */
		if (!fc_opclass->opcdefault)
			continue;
		if (fc_opclass->opcintype == fc_type_id)
		{
			fc_nexact++;
			fc_result = fc_opclass->oid;
		}
		else if (fc_nexact == 0 &&
				 IsBinaryCoercible(fc_type_id, fc_opclass->opcintype))
		{
			if (IsPreferredType(fc_tcategory, fc_opclass->opcintype))
			{
				fc_ncompatiblepreferred++;
				fc_result = fc_opclass->oid;
			}
			else if (fc_ncompatiblepreferred == 0)
			{
				fc_ncompatible++;
				fc_result = fc_opclass->oid;
			}
		}
	}

	systable_endscan(fc_scan);

	table_close(fc_rel, AccessShareLock);

	/* 如果 pg_opclass 包含不一致数据，则抛出错误 */
	if (fc_nexact > 1)
		ereport(ERROR,
				(errcode(ERRCODE_DUPLICATE_OBJECT),
				 errmsg("there are multiple default operator classes for data type %s",
						format_type_be(fc_type_id))));

	if (fc_nexact == 1 ||
		fc_ncompatiblepreferred == 1 ||
		(fc_ncompatiblepreferred == 0 && fc_ncompatible == 1))
		return fc_result;

	return InvalidOid;
}

/*
 *	makeObjectName()
 *
 *	为隐式创建的索引、序列、约束、扩展统计信息等创建一个名称。
 *
 *	参数通常包括：原始表名、原始字段名，以及一个“类型”字符串（如“seq”或“pkey”）。如果不相关，则字段名和/或类型可以为 NULL。
 *
 *	结果是一个 palloc'd 字符串。
 *
 *	我们想要的基本结果是“name1_name2_label”，当这些参数为 NULL 时省略“_name2”或“_label”。然而，我们必须生成一个字符数少于 NAMEDATALEN 的名称！因此，必要时我们将截断一个或两个名称以生成足够短的字符串。标签部分永远不会被截断（所以它最好是相对短）。
 *
 *	调用者负责检查生成名称的唯一性，并根据需要重试；重试将通过修改“label”字符串进行（这就是为什么我们从不截断那部分的原因）。
 */
char * makeObjectName(const char *fc_name1, const char *fc_name2, const char *fc_label)
{
	char	   *fc_name;
	int			fc_overhead = 0;	/* 标签和下划线所需的字符 */
	int			fc_availchars;		/* 可用于名称的字符 */
	int			fc_name1chars;		/* 分配给 name1 的字符 */
	int			fc_name2chars;		/* 分配给 name2 的字符 */
	int			fc_ndx;

	fc_name1chars = strlen(fc_name1);
	if (fc_name2)
	{
		fc_name2chars = strlen(fc_name2);
		fc_overhead++;				/* 允许分隔下划线 */
	}
	else
		fc_name2chars = 0;
	if (fc_label)
		fc_overhead += strlen(fc_label) + 1;

	fc_availchars = NAMEDATALEN - 1 - fc_overhead;
	Assert(fc_availchars > 0);		/* 否则调用者选择了一个错误的标签 */

	/*
	 * 如果我们必须截断，优先截断较长的名称。这个
	 * 逻辑可以不使用循环来表达，但以循环形式实现简单明了。
	 */
	while (fc_name1chars + fc_name2chars > fc_availchars)
	{
		if (fc_name1chars > fc_name2chars)
			fc_name1chars--;
		else
			fc_name2chars--;
	}

	fc_name1chars = pg_mbcliplen(fc_name1, fc_name1chars, fc_name1chars);
	if (fc_name2)
		fc_name2chars = pg_mbcliplen(fc_name2, fc_name2chars, fc_name2chars);

	/* 现在使用所选长度构建字符串 */
	fc_name = palloc(fc_name1chars + fc_name2chars + fc_overhead + 1);
	memcpy(fc_name, fc_name1, fc_name1chars);
	fc_ndx = fc_name1chars;
	if (fc_name2)
	{
		fc_name[fc_ndx++] = '_';
		memcpy(fc_name + fc_ndx, fc_name2, fc_name2chars);
		fc_ndx += fc_name2chars;
	}
	if (fc_label)
	{
		fc_name[fc_ndx++] = '_';
		strcpy(fc_name + fc_ndx, fc_label);
	}
	else
		fc_name[fc_ndx] = '\0';

	return fc_name;
}

/*
 * 为新关系选择一个不冲突的名称。通常用于选择索引名称（这就是它在这里的原因），但它也可以用于序列或任何自动生成的关系类型。
 *
 * name1、name2 和 label 的用法与 makeObjectName() 相同， 
 * 不同之处在于 label 不能为 NULL；如果需要创建一个在指定命名空间内唯一的名称，将在 label 末尾添加数字。
 *
 * 如果 isconstraint 为 true， 我们还会避免选择与同一命名空间内的任何现有约束匹配的名称。（这比 Postgres 自身要求的要严格，但 SQL 标准表示约束名称在模式内应唯一，因此我们对自动生成的约束名称遵循这一点。）
 *
 * 注意：理论上如果其他人同时选择相同的名称，仍然可能会出现冲突。 在实践中，这种问题发生的可能性相对较小，特别是在持有 name1 所识别的关系的独占锁时。然而，如果在单个命令中选择多个名称，最好在选择下一个之前创建新对象并进行 CommandCounterIncrement！
 *
 * 返回一个 palloc'd 字符串。
 */
char * ChooseRelationName(const char *fc_name1, const char *fc_name2,
				   const char *fc_label, Oid fc_namespaceid,
				   bool fc_isconstraint)
{
	int			fc_pass = 0;
	char	   *fc_relname = NULL;
	char		fc_modlabel[NAMEDATALEN];

	/* 首先尝试未修改的标签 */
	strlcpy(fc_modlabel, fc_label, sizeof(fc_modlabel));

	for (;;)
	{
		fc_relname = makeObjectName(fc_name1, fc_name2, fc_modlabel);

		if (!OidIsValid(get_relname_relid(fc_relname, fc_namespaceid)))
		{
			if (!fc_isconstraint ||
				!ConstraintNameExists(fc_relname, fc_namespaceid))
				break;
		}

		/* 发现冲突，因此尝试一个新的名称组件 */
		pfree(fc_relname);
		snprintf(fc_modlabel, sizeof(fc_modlabel), "%s%d", fc_label, ++fc_pass);
	}

	return fc_relname;
}

/*
 * 选择用于索引的名称。
 *
 * 参数列表相当随意 :-(
 */
static char * fc_ChooseIndexName(const char *fc_tabname, Oid fc_namespaceId,
				List *fc_colnames, List *fc_exclusionOpNames,
				bool fc_primary, bool fc_isconstraint)
{
	char	   *fc_indexname;

	if (fc_primary)
	{
		/* 主键的名称不依赖于特定的列 */
		fc_indexname = ChooseRelationName(fc_tabname,
									   NULL,
									   "pkey",
									   fc_namespaceId,
									   true);
	}
	else if (fc_exclusionOpNames != NIL)
	{
		fc_indexname = ChooseRelationName(fc_tabname,
									   fc_ChooseIndexNameAddition(fc_colnames),
									   "excl",
									   fc_namespaceId,
									   true);
	}
	else if (fc_isconstraint)
	{
		fc_indexname = ChooseRelationName(fc_tabname,
									   fc_ChooseIndexNameAddition(fc_colnames),
									   "key",
									   fc_namespaceId,
									   true);
	}
	else
	{
		fc_indexname = ChooseRelationName(fc_tabname,
									   fc_ChooseIndexNameAddition(fc_colnames),
									   "idx",
									   fc_namespaceId,
									   false);
	}

	return fc_indexname;
}

/*
 * 生成新的索引的“name2”，给定其列名列表
 * （由ChooseIndexColumnNames生成）。这将被传递给
 * ChooseRelationName，连同父表名称和适当的标签。
 *
 * 我们知道实际使用的字符少于NAMEDATALEN，
 * 因此我们可以在生成那么多之后截断结果。
 *
 * XXX 另见ChooseForeignKeyConstraintNameAddition和
 * ChooseExtendedStatisticNameAddition。
 */
static char * fc_ChooseIndexNameAddition(List *fc_colnames)
{
	char		fc_buf[NAMEDATALEN * 2];
	int			fc_buflen = 0;
	ListCell   *fc_lc;

	fc_buf[0] = '\0';
	foreach(fc_lc, fc_colnames)
	{
		const char *fc_name = (const char *) lfirst(fc_lc);

		if (fc_buflen > 0)
			fc_buf[fc_buflen++] = '_';	/* 在名称之间插入 _ */

		/*
		 * 此时我们有 buflen <= NAMEDATALEN。name 应该已经小于
		 * NAMEDATALEN，但为了安全使用 strlcpy。
		 */
		strlcpy(fc_buf + fc_buflen, fc_name, NAMEDATALEN);
		fc_buflen += strlen(fc_buf + fc_buflen);
		if (fc_buflen >= NAMEDATALEN)
			break;
	}
	return pstrdup(fc_buf);
}

/*
 * 选择用于索引列的实际名称，给定列的IndexElems列表。 
 * 这主要是关于确保名称是唯一的，以免出现冲突的属性名称错误。
 *
 * 返回一个普通字符串的列表（char *，而不是String节点）。
 */
static List * fc_ChooseIndexColumnNames(List *fc_indexElems)
{
	List	   *fc_result = NIL;
	ListCell   *fc_lc;

	foreach(fc_lc, fc_indexElems)
	{
		IndexElem  *fc_ielem = (IndexElem *) lfirst(fc_lc);
		const char *fc_origname;
		const char *fc_curname;
		int			fc_i;
		char		fc_buf[NAMEDATALEN];

		/* 从IndexElem获取初步名称 */
		if (fc_ielem->indexcolname)
			fc_origname = fc_ielem->indexcolname; /* 调用者指定的名称 */
		else if (fc_ielem->name)
			fc_origname = fc_ielem->name; /* 简单的列引用 */
		else
			fc_origname = "expr";	/* 表达式的默认名称 */

		/* 如果与任何先前的列冲突，进行调整 */
		fc_curname = fc_origname;
		for (fc_i = 1;; fc_i++)
		{
			ListCell   *fc_lc2;
			char		fc_nbuf[32];
			int			fc_nlen;

			foreach(fc_lc2, fc_result)
			{
				if (strcmp(fc_curname, (char *) lfirst(fc_lc2)) == 0)
					break;
			}
			if (fc_lc2 == NULL)
				break;			/* 找到不冲突的名称 */

			sprintf(fc_nbuf, "%d", fc_i);

			/* 确保生成的名称短于NAMEDATALEN */
			fc_nlen = pg_mbcliplen(fc_origname, strlen(fc_origname),
								NAMEDATALEN - 1 - strlen(fc_nbuf));
			memcpy(fc_buf, fc_origname, fc_nlen);
			strcpy(fc_buf + fc_nlen, fc_nbuf);
			fc_curname = fc_buf;
		}

		/* 并附加到结果列表 */
		fc_result = lappend(fc_result, pstrdup(fc_curname));
	}
	return fc_result;
}

/*
 * ExecReindex
 *
 * 手动REINDEX命令的主要入口点。这主要是一个
 * 为每个REINDEX子例程所发生的实际操作做准备的包装。
 */
void ExecReindex(ParseState *fc_pstate, ReindexStmt *fc_stmt, bool fc_isTopLevel)
{
	ReindexParams fc_params = {0};
	ListCell   *fc_lc;
	bool		fc_concurrently = false;
	bool		fc_verbose = false;
	char	   *fc_tablespacename = NULL;

	/* 解析选项列表 */
	foreach(fc_lc, fc_stmt->params)
	{
		DefElem    *fc_opt = (DefElem *) lfirst(fc_lc);

		if (strcmp(fc_opt->defname, "verbose") == 0)
			fc_verbose = defGetBoolean(fc_opt);
		else if (strcmp(fc_opt->defname, "concurrently") == 0)
			fc_concurrently = defGetBoolean(fc_opt);
		else if (strcmp(fc_opt->defname, "tablespace") == 0)
			fc_tablespacename = defGetString(fc_opt);
		else
			ereport(ERROR,
					(errcode(ERRCODE_SYNTAX_ERROR),
					 errmsg("unrecognized REINDEX option \"%s\"",
							fc_opt->defname),
					 parser_errposition(fc_pstate, fc_opt->location)));
	}

	if (fc_concurrently)
		PreventInTransactionBlock(fc_isTopLevel,
								  "REINDEX CONCURRENTLY");

	fc_params.options =
		(fc_verbose ? REINDEXOPT_VERBOSE : 0) |
		(fc_concurrently ? REINDEXOPT_CONCURRENTLY : 0);

	/*
	 * 分配要移动索引的表空间OID，使用InvalidOid不执行任何操作。
	 */
	if (fc_tablespacename != NULL)
	{
		fc_params.tablespaceOid = get_tablespace_oid(fc_tablespacename, false);

		/* 检查权限，除非移动到数据库的默认值 */
		if (OidIsValid(fc_params.tablespaceOid) &&
			fc_params.tablespaceOid != MyDatabaseTableSpace)
		{
			AclResult	fc_aclresult;

			fc_aclresult = pg_tablespace_aclcheck(fc_params.tablespaceOid,
											   GetUserId(), ACL_CREATE);
			if (fc_aclresult != ACLCHECK_OK)
				aclcheck_error(fc_aclresult, OBJECT_TABLESPACE,
							   get_tablespace_name(fc_params.tablespaceOid));
		}
	}
	else
		fc_params.tablespaceOid = InvalidOid;

	switch (fc_stmt->kind)
	{
		case REINDEX_OBJECT_INDEX:
			fc_ReindexIndex(fc_stmt->relation, &fc_params, fc_isTopLevel);
			break;
		case REINDEX_OBJECT_TABLE:
			fc_ReindexTable(fc_stmt->relation, &fc_params, fc_isTopLevel);
			break;
		case REINDEX_OBJECT_SCHEMA:
		case REINDEX_OBJECT_SYSTEM:
		case REINDEX_OBJECT_DATABASE:

			/*
			 * 这不能在用户事务块内运行；如果我们
			 * 在一个事务内，那么它的提交和
			 * 启动事务命令调用将不会产生预期
			 * 效果！
			 */
			PreventInTransactionBlock(fc_isTopLevel,
									  (fc_stmt->kind == REINDEX_OBJECT_SCHEMA) ? "REINDEX SCHEMA" :
									  (fc_stmt->kind == REINDEX_OBJECT_SYSTEM) ? "REINDEX SYSTEM" :
									  "REINDEX DATABASE");
			fc_ReindexMultipleTables(fc_stmt->name, fc_stmt->kind, &fc_params);
			break;
		default:
			elog(ERROR, "unrecognized object type: %d",
				 (int) fc_stmt->kind);
			break;
	}
}

/*
 * ReindexIndex
 *		重新创建一个特定的索引。
 */
static void fc_ReindexIndex(RangeVar *fc_indexRelation, ReindexParams *fc_params, bool fc_isTopLevel)
{
	struct ReindexIndexCallbackState fc_state;
	Oid			fc_indOid;
	char		fc_persistence;
	char		fc_relkind;

	/*
	 * 查找并锁定索引，检查表的权限；使用回调
	 * 首先获取表的锁，以避免死锁风险。这里使用的锁定级别
	 * 必须与在reindex_index()中获得的索引锁匹配。
	 *
	 * 如果是临时索引，我们将执行非并发重建，
	 * 即使请求了CONCURRENTLY。在这种情况下，reindex_index()将
	 * 升级锁定，但这没关系，因为其他会话不能持有
	 * 对我们临时表的锁。
	 */
	fc_state.params = *fc_params;
	fc_state.locked_table_oid = InvalidOid;
	fc_indOid = RangeVarGetRelidExtended(fc_indexRelation,
									  (fc_params->options & REINDEXOPT_CONCURRENTLY) != 0 ?
									  ShareUpdateExclusiveLock : AccessExclusiveLock,
									  0,
									  fc_RangeVarCallbackForReindexIndex,
									  &fc_state);

	/*
	 * 获取现有索引的当前持久性和类型。我们
	 * 已经持有对该索引的锁。
	 */
	fc_persistence = get_rel_persistence(fc_indOid);
	fc_relkind = get_rel_relkind(fc_indOid);

	if (fc_relkind == RELKIND_PARTITIONED_INDEX)
		fc_ReindexPartitions(fc_indOid, fc_params, fc_isTopLevel);
	else if ((fc_params->options & REINDEXOPT_CONCURRENTLY) != 0 &&
			 fc_persistence != RELPERSISTENCE_TEMP)
		fc_ReindexRelationConcurrently(fc_indOid, fc_params);
	else
	{
		ReindexParams fc_newparams = *fc_params;

		fc_newparams.options |= REINDEXOPT_REPORT_PROGRESS;
		reindex_index(fc_indOid, false, fc_persistence, &fc_newparams);
	}
}

/*
 * 在获取关系锁之前检查表的权限；还要在RangeVarGetRelidExtended获取索引锁之前锁定
 * 堆，以避免死锁。
 */
static void fc_RangeVarCallbackForReindexIndex(const RangeVar *fc_relation,
								Oid fc_relId, Oid fc_oldRelId, void *fc_arg)
{
	char		fc_relkind;
	struct ReindexIndexCallbackState *fc_state = fc_arg;
	LOCKMODE	fc_table_lockmode;

	/*
	 * 这里的锁定级别应该与reindex_index()中
	 * 的表锁匹配，以便在非并发情况下和用于
	 * 并发情况下的index_concurrently_*()表锁匹配。
	 */
	fc_table_lockmode = (fc_state->params.options & REINDEXOPT_CONCURRENTLY) != 0 ?
		ShareUpdateExclusiveLock : ShareLock;

	/*
	 * 如果我们之前锁定了其他索引的堆，并且我们要查找的名称
	 * 不再指向该关系，则释放现在无用的锁。
	 */
	if (fc_relId != fc_oldRelId && OidIsValid(fc_oldRelId))
	{
		UnlockRelationOid(fc_state->locked_table_oid, fc_table_lockmode);
		fc_state->locked_table_oid = InvalidOid;
	}

	/* 如果关系不存在，就没有更多的事情可做。 */
	if (!OidIsValid(fc_relId))
		return;

	/*
	 * 如果关系确实存在，检查它是否是一个索引。但请注意，
	 * 在我们进行名称查找和现在之间，关系可能已经被删除。在这种情况下，没什么可做的。
	 */
	fc_relkind = get_rel_relkind(fc_relId);
	if (!fc_relkind)
		return;
	if (fc_relkind != RELKIND_INDEX &&
		fc_relkind != RELKIND_PARTITIONED_INDEX)
		ereport(ERROR,
				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
				 errmsg("\"%s\" is not an index", fc_relation->relname)));

	/* 检查权限 */
	if (!pg_class_ownercheck(fc_relId, GetUserId()))
		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_INDEX, fc_relation->relname);

	/* 在索引之前锁定堆以避免死锁。 */
	if (fc_relId != fc_oldRelId)
	{
		Oid			fc_table_oid = IndexGetRelation(fc_relId, true);

		/*
		 * 如果 OID 无效，意味着索引被并发删除，
		 * 这对我们来说不是问题；只需正常返回。
		 */
		if (OidIsValid(fc_table_oid))
		{
			LockRelationOid(fc_table_oid, fc_table_lockmode);
			fc_state->locked_table_oid = fc_table_oid;
		}
	}
}

/*
 * ReindexTable
 *		重新创建一个表的所有索引（如果有的话，也包括其 toast 表）
 */
static Oid fc_ReindexTable(RangeVar *fc_relation, ReindexParams *fc_params, bool fc_isTopLevel)
{
	Oid			fc_heapOid;
	bool		fc_result;

	/*
	 * 此处使用的锁定级别应与 reindex_relation() 匹配。
	 *
	 * 如果是临时表，我们将执行非并发的重新索引，
	 * 即使请求了 CONCURRENTLY。在这种情况下，reindex_relation()
	 * 会升级锁，但这没有问题，因为其他会话不能在我们的临时表上保持锁。
	 */
	fc_heapOid = RangeVarGetRelidExtended(fc_relation,
									   (fc_params->options & REINDEXOPT_CONCURRENTLY) != 0 ?
									   ShareUpdateExclusiveLock : ShareLock,
									   0,
									   RangeVarCallbackOwnsTable, NULL);

	if (get_rel_relkind(fc_heapOid) == RELKIND_PARTITIONED_TABLE)
		fc_ReindexPartitions(fc_heapOid, fc_params, fc_isTopLevel);
	else if ((fc_params->options & REINDEXOPT_CONCURRENTLY) != 0 &&
			 get_rel_persistence(fc_heapOid) != RELPERSISTENCE_TEMP)
	{
		fc_result = fc_ReindexRelationConcurrently(fc_heapOid, fc_params);

		if (!fc_result)
			ereport(NOTICE,
					(errmsg("table \"%s\" has no indexes that can be reindexed concurrently",
							fc_relation->relname)));
	}
	else
	{
		ReindexParams fc_newparams = *fc_params;

		fc_newparams.options |= REINDEXOPT_REPORT_PROGRESS;
		fc_result = reindex_relation(fc_heapOid,
								  REINDEX_REL_PROCESS_TOAST |
								  REINDEX_REL_CHECK_CONSTRAINTS,
								  &fc_newparams);
		if (!fc_result)
			ereport(NOTICE,
					(errmsg("table \"%s\" has no indexes to reindex",
							fc_relation->relname)));
	}

	return fc_heapOid;
}

/*
 * ReindexMultipleTables
 *		重新创建由 objectName/objectKind 选择的表的索引。
 *
 * 为了减少死锁的概率，每个表在单独的事务中重新索引，
 * 这样我们可以立即释放对它的锁。
 * 这意味着这不能在用户事务块内调用！
 */
static void fc_ReindexMultipleTables(const char *fc_objectName, ReindexObjectType fc_objectKind,
					  ReindexParams *fc_params)
{
	Oid			fc_objectOid;
	Relation	fc_relationRelation;
	TableScanDesc fc_scan;
	ScanKeyData fc_scan_keys[1];
	HeapTuple	fc_tuple;
	MemoryContext fc_private_context;
	MemoryContext fc_old;
	List	   *fc_relids = NIL;
	int			fc_num_keys;
	bool		fc_concurrent_warning = false;
	bool		fc_tablespace_warning = false;

	AssertArg(fc_objectName);
	Assert(fc_objectKind == REINDEX_OBJECT_SCHEMA ||
		   fc_objectKind == REINDEX_OBJECT_SYSTEM ||
		   fc_objectKind == REINDEX_OBJECT_DATABASE);

	if (fc_objectKind == REINDEX_OBJECT_SYSTEM &&
		(fc_params->options & REINDEXOPT_CONCURRENTLY) != 0)
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("cannot reindex system catalogs concurrently")));

	/*
	 * 获取要重新索引的对象的 OID，该对象是会话当前使用的数据库
	 * 或系统目录，或调用者定义的模式。同时执行权限检查，
	 * 根据对象类型需要不同的处理。
	 */
	if (fc_objectKind == REINDEX_OBJECT_SCHEMA)
	{
		fc_objectOid = get_namespace_oid(fc_objectName, false);

		if (!pg_namespace_ownercheck(fc_objectOid, GetUserId()))
			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SCHEMA,
						   fc_objectName);
	}
	else
	{
		fc_objectOid = MyDatabaseId;

		if (strcmp(fc_objectName, get_database_name(fc_objectOid)) != 0)
			ereport(ERROR,
					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
					 errmsg("can only reindex the currently open database")));
		if (!pg_database_ownercheck(fc_objectOid, GetUserId()))
			aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_DATABASE,
						   fc_objectName);
	}

	/*
	 * 创建一个内存上下文，即使在我们下面强制提交事务时也能存活。
	 * 由于它是 PortalContext 的子上下文，即使我们遇到错误它也最终会消失；
	 * 不需要特殊的中止清理逻辑。
	 */
	fc_private_context = AllocSetContextCreate(PortalContext,
											"ReindexMultipleTables",
											ALLOCSET_SMALL_SIZES);

	/*
	 * 定义搜索键以查找要重新索引的对象。对于模式，我们
	 * 使用 relnamespace 选择目标关系，这在全数据库操作中并不必要。
	 */
	if (fc_objectKind == REINDEX_OBJECT_SCHEMA)
	{
		fc_num_keys = 1;
		ScanKeyInit(&fc_scan_keys[0],
					Anum_pg_class_relnamespace,
					BTEqualStrategyNumber, F_OIDEQ,
					ObjectIdGetDatum(fc_objectOid));
	}
	else
		fc_num_keys = 0;

	/*
	 * 扫描 pg_class 以构建我们需要重新索引的关系列表。
	 *
	 * 我们这里只考虑普通关系和物化视图（toast
	 * 关系将通过 reindex_relation 间接处理）。
	 */
	fc_relationRelation = table_open(RelationRelationId, AccessShareLock);
	fc_scan = table_beginscan_catalog(fc_relationRelation, fc_num_keys, fc_scan_keys);
	while ((fc_tuple = heap_getnext(fc_scan, ForwardScanDirection)) != NULL)
	{
		Form_pg_class fc_classtuple = (Form_pg_class) GETSTRUCT(fc_tuple);
		Oid			fc_relid = fc_classtuple->oid;

		/*
		 * 只有常规表和物化视图可以有索引，因此忽略任何
		 * 其他类型的关系。
		 *
		 * 分区表/索引被跳过，但匹配的叶子分区会被处理。
		 */
		if (fc_classtuple->relkind != RELKIND_RELATION &&
			fc_classtuple->relkind != RELKIND_MATVIEW)
			continue;

		/* 跳过其他后端的临时表；我们根本无法重新索引它们。 */
		if (fc_classtuple->relpersistence == RELPERSISTENCE_TEMP &&
			!isTempNamespace(fc_classtuple->relnamespace))
			continue;

		/* 检查用户/系统分类，并可选地跳过 */
		if (fc_objectKind == REINDEX_OBJECT_SYSTEM &&
			!IsSystemClass(fc_relid, fc_classtuple))
			continue;

		/*
		 * 如果用户是超级用户、表的拥有者或数据库/模式的拥有者，该表可以重新索引（但在后者的情况下，仅当它不是共享关系时）。pg_class_ownercheck 包括超级用户情况，并且根据 objectKind，我们已经知道用户有权限在此数据库或模式上运行 REINDEX，根据本例程序开头的权限检查。
		 */
		if (fc_classtuple->relisshared &&
			!pg_class_ownercheck(fc_relid, GetUserId()))
			continue;

		/*
		 * 跳过系统表，因为 index_create() 会拒绝同时对它们进行索引（如果我们尝试，这可能会失败）。
		 */
		if ((fc_params->options & REINDEXOPT_CONCURRENTLY) != 0 &&
			IsCatalogRelationOid(fc_relid))
		{
			if (!fc_concurrent_warning)
				ereport(WARNING,
						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
						 errmsg("cannot reindex system catalogs concurrently, skipping all")));
			fc_concurrent_warning = true;
			continue;
		}

		/*
		 * 如果设置了新的表空间，检查是否需要跳过此关系。
		 */
		if (OidIsValid(fc_params->tablespaceOid))
		{
			bool		fc_skip_rel = false;

			/*
			 * 映射的关系无法移动到不同的表空间（特别是这消除了所有共享目录）。
			 */
			if (RELKIND_HAS_STORAGE(fc_classtuple->relkind) &&
				!OidIsValid(fc_classtuple->relfilenode))
				fc_skip_rel = true;

			/*
			 * 系统关系始终会被跳过，即使启用了 allow_system_table_mods。
			 */
			if (IsSystemClass(fc_relid, fc_classtuple))
				fc_skip_rel = true;

			if (fc_skip_rel)
			{
				if (!fc_tablespace_warning)
					ereport(WARNING,
							(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
							 errmsg("cannot move system relations, skipping all")));
				fc_tablespace_warning = true;
				continue;
			}
		}

		/* 在私有上下文中保存关系 OID 列表 */
		fc_old = MemoryContextSwitchTo(fc_private_context);

		/*
		 * 如果 pg_class 被选中进行重新索引，我们始终希望首先重新索引 pg_class。这确保如果 pg_class 的索引存在任何损坏，它们将在处理任何其他表之前被修复。这是关键，因为重新索引本身会尝试更新 pg_class。
		 */
		if (fc_relid == RelationRelationId)
			fc_relids = lcons_oid(fc_relid, fc_relids);
		else
			fc_relids = lappend_oid(fc_relids, fc_relid);

		MemoryContextSwitchTo(fc_old);
	}
	table_endscan(fc_scan);
	table_close(fc_relationRelation, AccessShareLock);

	/*
	 * 在单独的事务中处理列表中的每个关系。请注意，这会提交，然后立即启动一个新的事务。
	 */
	fc_ReindexMultipleInternal(fc_relids, fc_params);

	MemoryContextDelete(fc_private_context);
}

/*
 * 特定于 ReindexPartitions() 的错误回调。
 */
static void fc_reindex_error_callback(void *fc_arg)
{
	ReindexErrorInfo *fc_errinfo = (ReindexErrorInfo *) fc_arg;

	Assert(RELKIND_HAS_PARTITIONS(fc_errinfo->relkind));

	if (fc_errinfo->relkind == RELKIND_PARTITIONED_TABLE)
		errcontext("while reindexing partitioned table \"%s.%s\"",
				   fc_errinfo->relnamespace, fc_errinfo->relname);
	else if (fc_errinfo->relkind == RELKIND_PARTITIONED_INDEX)
		errcontext("while reindexing partitioned index \"%s.%s\"",
				   fc_errinfo->relnamespace, fc_errinfo->relname);
}

/*
 * ReindexPartitions
 *
 * 根据调用者提供的分区索引或表重新索引一组分区。
 */
static void fc_ReindexPartitions(Oid fc_relid, ReindexParams *fc_params, bool fc_isTopLevel)
{
	List	   *fc_partitions = NIL;
	char		fc_relkind = get_rel_relkind(fc_relid);
	char	   *fc_relname = get_rel_name(fc_relid);
	char	   *fc_relnamespace = get_namespace_name(get_rel_namespace(fc_relid));
	MemoryContext fc_reindex_context;
	List	   *fc_inhoids;
	ListCell   *fc_lc;
	ErrorContextCallback fc_errcallback;
	ReindexErrorInfo fc_errinfo;

	Assert(RELKIND_HAS_PARTITIONS(fc_relkind));

	/*
	 * 检查此操作是否在事务块中运行，并提供错误回调以提供更多上下文信息。
	 */
	fc_errinfo.relname = pstrdup(fc_relname);
	fc_errinfo.relnamespace = pstrdup(fc_relnamespace);
	fc_errinfo.relkind = fc_relkind;
	fc_errcallback.callback = fc_reindex_error_callback;
	fc_errcallback.arg = (void *) &fc_errinfo;
	fc_errcallback.previous = error_context_stack;
	error_context_stack = &fc_errcallback;

	PreventInTransactionBlock(fc_isTopLevel,
							  fc_relkind == RELKIND_PARTITIONED_TABLE ?
							  "REINDEX TABLE" : "REINDEX INDEX");

	/* 弹出错误上下文栈 */
	error_context_stack = fc_errcallback.previous;

	/*
	 * 为跨事务存储创建特殊内存上下文。
	 *
	 * 由于它是 PortalContext 的子项，即使我们遭遇错误也会最终消失，因此不需要特殊的中止清理逻辑。
	 */
	fc_reindex_context = AllocSetContextCreate(PortalContext, "Reindex",
											ALLOCSET_DEFAULT_SIZES);

	/* ShareLock 足以防止模式修改 */
	fc_inhoids = find_all_inheritors(fc_relid, ShareLock, NULL);

	/*
	 * 要重新索引的关系列表是树的物理分区，因此丢弃任何分区表或索引。
	 */
	foreach(fc_lc, fc_inhoids)
	{
		Oid			fc_partoid = lfirst_oid(fc_lc);
		char		fc_partkind = get_rel_relkind(fc_partoid);
		MemoryContext fc_old_context;

		/*
		 * 这将丢弃分区表、分区索引和外部表。
		 */
		if (!RELKIND_HAS_STORAGE(fc_partkind))
			continue;

		Assert(fc_partkind == RELKIND_INDEX ||
			   fc_partkind == RELKIND_RELATION);

		/* 保存分区 OID */
		fc_old_context = MemoryContextSwitchTo(fc_reindex_context);
		fc_partitions = lappend_oid(fc_partitions, fc_partoid);
		MemoryContextSwitchTo(fc_old_context);
	}

	/*
	 * 在单独的事务中处理列表中的每个分区。请注意，这会提交，然后立即启动一个新的事务。
	 */
	fc_ReindexMultipleInternal(fc_partitions, fc_params);

	/*
	 * 清理工作存储 --- 请注意，我们必须在 StartTransactionCommand 之后这样做，否则我们可能试图删除活动上下文！
	 */
	MemoryContextDelete(fc_reindex_context);
}

/*
 * ReindexMultipleInternal
 *
 * 重新索引一系列关系，每个关系在各自的事务中处理。这会立即提交现有事务，并在完成时启动新事务。
 */
static void fc_ReindexMultipleInternal(List *fc_relids, ReindexParams *fc_params)
{
	ListCell   *fc_l;

	PopActiveSnapshot();
	CommitTransactionCommand();

	foreach(fc_l, fc_relids)
	{
		Oid			fc_relid = lfirst_oid(fc_l);
		char		fc_relkind;
		char		fc_relpersistence;

		StartTransactionCommand();

		/* 索引中的函数可能需要设置快照 */
		PushActiveSnapshot(GetTransactionSnapshot());

		/* 检查关系是否仍然存在 */
		if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(fc_relid)))
		{
			PopActiveSnapshot();
			CommitTransactionCommand();
			continue;
		}

		/*
		 * 在选择新的表空间时，检查权限，除非移动到数据库的默认表空间。请注意，此检查也发生在
		 * ExecReindex() 中，但由于它跨多个事务运行，我们在这里进行额外检查。
		 */
		if (OidIsValid(fc_params->tablespaceOid) &&
			fc_params->tablespaceOid != MyDatabaseTableSpace)
		{
			AclResult	fc_aclresult;

			fc_aclresult = pg_tablespace_aclcheck(fc_params->tablespaceOid,
											   GetUserId(), ACL_CREATE);
			if (fc_aclresult != ACLCHECK_OK)
				aclcheck_error(fc_aclresult, OBJECT_TABLESPACE,
							   get_tablespace_name(fc_params->tablespaceOid));
		}

		fc_relkind = get_rel_relkind(fc_relid);
		fc_relpersistence = get_rel_persistence(fc_relid);

		/*
		 * 分区表和索引不能直接处理，应该首先构建其叶节点的列表。
		 */
		Assert(!RELKIND_HAS_PARTITIONS(fc_relkind));

		if ((fc_params->options & REINDEXOPT_CONCURRENTLY) != 0 &&
			fc_relpersistence != RELPERSISTENCE_TEMP)
		{
			ReindexParams fc_newparams = *fc_params;

			fc_newparams.options |= REINDEXOPT_MISSING_OK;
			(void) fc_ReindexRelationConcurrently(fc_relid, &fc_newparams);
			/* ReindexRelationConcurrently() 进行详细输出 */
		}
		else if (fc_relkind == RELKIND_INDEX)
		{
			ReindexParams fc_newparams = *fc_params;

			fc_newparams.options |=
				REINDEXOPT_REPORT_PROGRESS | REINDEXOPT_MISSING_OK;
			reindex_index(fc_relid, false, fc_relpersistence, &fc_newparams);
			PopActiveSnapshot();
			/* reindex_index() 进行详细输出 */
		}
		else
		{
			bool		fc_result;
			ReindexParams fc_newparams = *fc_params;

			fc_newparams.options |=
				REINDEXOPT_REPORT_PROGRESS | REINDEXOPT_MISSING_OK;
			fc_result = reindex_relation(fc_relid,
									  REINDEX_REL_PROCESS_TOAST |
									  REINDEX_REL_CHECK_CONSTRAINTS,
									  &fc_newparams);

			if (fc_result && (fc_params->options & REINDEXOPT_VERBOSE) != 0)
				ereport(INFO,
						(errmsg("table \"%s.%s\" was reindexed",
								get_namespace_name(get_rel_namespace(fc_relid)),
								get_rel_name(fc_relid))));

			PopActiveSnapshot();
		}

		CommitTransactionCommand();
	}

	StartTransactionCommand();
}


/*
 * ReindexRelationConcurrently - 对给定关系 OID 处理 REINDEX CONCURRENTLY
 *
 * 'relationOid' 可以属于索引、表或物化视图。对于表和物化视图，将重建其所有索引，
 * 排除无效索引和任何用于排除约束的索引，但包括其相关的 toast 表索引。对于索引，
 * 将重建索引本身。
 *
 * 对父表和相关索引采取的锁将在事务提交之前保持，在那时对每个关系采取会话锁。
 * 这两者都可以防止并发的架构更改。
 *
 * 如果重建了任何索引（包括在相关情况下的 toast 表索引），则返回真；否则返回假。
 *
 * 注意：这不能用于临时关系。并发构建会导致与并发构建事务触发的 ON COMMIT 操作出现问题。
 * 临时关系不受并发问题的影响，因此并不需要更复杂的并发构建，而非并发重建更高效。
 */
static bool fc_ReindexRelationConcurrently(Oid fc_relationOid, ReindexParams *fc_params)
{
	typedef struct ReindexIndexInfo
	{
		Oid			indexId;
		Oid			tableId;
		Oid			amId;
		bool		safe;		/* 用于 set_indexsafe_procflags */
	} ReindexIndexInfo;
	List	   *fc_heapRelationIds = NIL;
	List	   *fc_indexIds = NIL;
	List	   *fc_newIndexIds = NIL;
	List	   *fc_relationLocks = NIL;
	List	   *fc_lockTags = NIL;
	ListCell   *fc_lc,
			   *fc_lc2;
	MemoryContext fc_private_context;
	MemoryContext fc_oldcontext;
	char		fc_relkind;
	char	   *fc_relationName = NULL;
	char	   *fc_relationNamespace = NULL;
	PGRUsage	fc_ru0;
	const int	fc_progress_index[] = {
		PROGRESS_CREATEIDX_COMMAND,
		PROGRESS_CREATEIDX_PHASE,
		PROGRESS_CREATEIDX_INDEX_OID,
		PROGRESS_CREATEIDX_ACCESS_METHOD_OID
	};
	int64		fc_progress_vals[4];

	/*
	 * 创建一个内存上下文，即使在我们下面强制提交事务时也能存活。
	 * 由于它是 PortalContext 的子上下文，即使我们遇到错误它也最终会消失；
	 * 不需要特殊的中止清理逻辑。
	 */
	fc_private_context = AllocSetContextCreate(PortalContext,
											"ReindexConcurrent",
											ALLOCSET_SMALL_SIZES);

	if ((fc_params->options & REINDEXOPT_VERBOSE) != 0)
	{
		/* 在私有上下文中保存 REINDEX VERBOSE 需要的数据 */
		fc_oldcontext = MemoryContextSwitchTo(fc_private_context);

		fc_relationName = get_rel_name(fc_relationOid);
		fc_relationNamespace = get_namespace_name(get_rel_namespace(fc_relationOid));

		pg_rusage_init(&fc_ru0);

		MemoryContextSwitchTo(fc_oldcontext);
	}

	fc_relkind = get_rel_relkind(fc_relationOid);

	/*
	 * 根据调用者提供的关系 Oid 提取将要重建的索引列表。
	 */
	switch (fc_relkind)
	{
		case RELKIND_RELATION:
		case RELKIND_MATVIEW:
		case RELKIND_TOASTVALUE:
			{
				/*
				 * 在关系的情况下，查找其所有索引，包括 toast 索引。
				 */
				Relation	fc_heapRelation;

				/* 在私有上下文中保存关系 OID 列表 */
				fc_oldcontext = MemoryContextSwitchTo(fc_private_context);

				/* 跟踪此关系以便进行会话锁定 */
				fc_heapRelationIds = lappend_oid(fc_heapRelationIds, fc_relationOid);

				MemoryContextSwitchTo(fc_oldcontext);

				if (IsCatalogRelationOid(fc_relationOid))
					ereport(ERROR,
							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
							 errmsg("cannot reindex system catalogs concurrently")));

				/* 打开关系以获取其索引 */
				if ((fc_params->options & REINDEXOPT_MISSING_OK) != 0)
				{
					fc_heapRelation = try_table_open(fc_relationOid,
												  ShareUpdateExclusiveLock);
					/* 如果关系不存在则离开 */
					if (!fc_heapRelation)
						break;
				}
				else
					fc_heapRelation = table_open(fc_relationOid,
											  ShareUpdateExclusiveLock);

				if (OidIsValid(fc_params->tablespaceOid) &&
					IsSystemRelation(fc_heapRelation))
					ereport(ERROR,
							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
							 errmsg("cannot move system relation \"%s\"",
									RelationGetRelationName(fc_heapRelation))));

				/* 将关系的所有有效索引添加到列表中 */
				foreach(fc_lc, RelationGetIndexList(fc_heapRelation))
				{
					Oid			fc_cellOid = lfirst_oid(fc_lc);
					Relation	fc_indexRelation = index_open(fc_cellOid,
														   ShareUpdateExclusiveLock);

					if (!fc_indexRelation->rd_index->indisvalid)
						ereport(WARNING,
								(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
								 errmsg("cannot reindex invalid index \"%s.%s\" concurrently, skipping",
										get_namespace_name(get_rel_namespace(fc_cellOid)),
										get_rel_name(fc_cellOid))));
					else if (fc_indexRelation->rd_index->indisexclusion)
						ereport(WARNING,
								(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
								 errmsg("cannot reindex exclusion constraint index \"%s.%s\" concurrently, skipping",
										get_namespace_name(get_rel_namespace(fc_cellOid)),
										get_rel_name(fc_cellOid))));
					else
					{
						ReindexIndexInfo *fc_idx;

						/* 在私有上下文中保存关系 OID 列表 */
						fc_oldcontext = MemoryContextSwitchTo(fc_private_context);

						fc_idx = palloc(sizeof(ReindexIndexInfo));
						fc_idx->indexId = fc_cellOid;
						/* 其他字段稍后设置 */

						fc_indexIds = lappend(fc_indexIds, fc_idx);

						MemoryContextSwitchTo(fc_oldcontext);
					}

					index_close(fc_indexRelation, NoLock);
				}

				/* 还添加 toast 索引 */
				if (OidIsValid(fc_heapRelation->rd_rel->reltoastrelid))
				{
					Oid			fc_toastOid = fc_heapRelation->rd_rel->reltoastrelid;
					Relation	fc_toastRelation = table_open(fc_toastOid,
														   ShareUpdateExclusiveLock);

					/* 在私有上下文中保存关系 OID 列表 */
					fc_oldcontext = MemoryContextSwitchTo(fc_private_context);

					/* 跟踪此关系以便进行会话锁定 */
					fc_heapRelationIds = lappend_oid(fc_heapRelationIds, fc_toastOid);

					MemoryContextSwitchTo(fc_oldcontext);

					foreach(fc_lc2, RelationGetIndexList(fc_toastRelation))
					{
						Oid			fc_cellOid = lfirst_oid(fc_lc2);
						Relation	fc_indexRelation = index_open(fc_cellOid,
															   ShareUpdateExclusiveLock);

						if (!fc_indexRelation->rd_index->indisvalid)
							ereport(WARNING,
									(errcode(ERRCODE_INDEX_CORRUPTED),
									 errmsg("cannot reindex invalid index \"%s.%s\" concurrently, skipping",
											get_namespace_name(get_rel_namespace(fc_cellOid)),
											get_rel_name(fc_cellOid))));
						else
						{
							ReindexIndexInfo *fc_idx;

							/*
							 * 在私有上下文中保存关系 OIDs 的列表
							 */
							fc_oldcontext = MemoryContextSwitchTo(fc_private_context);

							fc_idx = palloc(sizeof(ReindexIndexInfo));
							fc_idx->indexId = fc_cellOid;
							fc_indexIds = lappend(fc_indexIds, fc_idx);
							/* 其他字段稍后设置 */

							MemoryContextSwitchTo(fc_oldcontext);
						}

						index_close(fc_indexRelation, NoLock);
					}

					table_close(fc_toastRelation, NoLock);
				}

				table_close(fc_heapRelation, NoLock);
				break;
			}
		case RELKIND_INDEX:
			{
				Oid			fc_heapId = IndexGetRelation(fc_relationOid,
													  (fc_params->options & REINDEXOPT_MISSING_OK) != 0);
				Relation	fc_heapRelation;
				ReindexIndexInfo *fc_idx;

				/* 如果关系缺失，则离开 */
				if (!OidIsValid(fc_heapId))
					break;

				if (IsCatalogRelationOid(fc_heapId))
					ereport(ERROR,
							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
							 errmsg("cannot reindex system catalogs concurrently")));

				/*
				 * 不允许对 TOAST 表上的无效索引进行重建，因为如果重建，将无法删除它。匹配
				 * reindex_index() 中的错误信息。
				 */
				if (IsToastNamespace(get_rel_namespace(fc_relationOid)) &&
					!get_index_isvalid(fc_relationOid))
					ereport(ERROR,
							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
							 errmsg("cannot reindex invalid index on TOAST table")));

				/*
				 * 检查父关系是否可以被锁定以及是否存在，
				 * 这需要在此阶段完成，因为要重建的索引列表
				 * 还不完整，一旦所有会话锁被获取，REINDEXOPT_MISSING_OK
				 * 不应被使用。
				 */
				if ((fc_params->options & REINDEXOPT_MISSING_OK) != 0)
				{
					fc_heapRelation = try_table_open(fc_heapId,
												  ShareUpdateExclusiveLock);
					/* 如果关系不存在则离开 */
					if (!fc_heapRelation)
						break;
				}
				else
					fc_heapRelation = table_open(fc_heapId,
											  ShareUpdateExclusiveLock);

				if (OidIsValid(fc_params->tablespaceOid) &&
					IsSystemRelation(fc_heapRelation))
					ereport(ERROR,
							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
							 errmsg("cannot move system relation \"%s\"",
									get_rel_name(fc_relationOid))));

				table_close(fc_heapRelation, NoLock);

				/* 在私有上下文中保存关系 OID 列表 */
				fc_oldcontext = MemoryContextSwitchTo(fc_private_context);

				/* 跟踪此索引的堆关系以便于会话锁定 */
				fc_heapRelationIds = list_make1_oid(fc_heapId);

				/*
				 * 在私有上下文中保存关系OID列表。注意
				 * 这里允许无效索引存在。
				 */
				fc_idx = palloc(sizeof(ReindexIndexInfo));
				fc_idx->indexId = fc_relationOid;
				fc_indexIds = lappend(fc_indexIds, fc_idx);
				/* 其他字段稍后设置 */

				MemoryContextSwitchTo(fc_oldcontext);
				break;
			}

		case RELKIND_PARTITIONED_TABLE:
		case RELKIND_PARTITIONED_INDEX:
		default:
			/* 如果关系的类型不受支持，则返回错误 */
			ereport(ERROR,
					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
					 errmsg("cannot reindex this type of relation concurrently")));
			break;
	}

	/*
	 * 确定没有索引，所以离开。任何基于
	 * REINDEXOPT_MISSING_OK 的检查应仅在构建要
	 * 处理的索引列表时进行，因为在此事务提交之前
	 * 获取的会话锁将确保在此操作完成之前不能被并发会话删除。
	 */
	if (fc_indexIds == NIL)
	{
		PopActiveSnapshot();
		return false;
	}

	/* 这不是一个共享目录，因此拒绝将其移动到共享表空间 */
	if (fc_params->tablespaceOid == GLOBALTABLESPACE_OID)
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("cannot move non-shared relation to tablespace \"%s\"",
						get_tablespace_name(fc_params->tablespaceOid))));

	Assert(fc_heapRelationIds != NIL);

	/*-----
	 * 现在我们有了要处理的所有索引在 indexIds 中。
	 *
	 * 当前阶段为：
	 *
	 * 1. 在目录中创建新索引
	 * 2. 构建新索引
	 * 3. 让新索引赶上期间插入的元组
	 * 4. 交换索引名称
	 * 5. 将旧索引标记为无效
	 * 6. 删除旧索引
	 *
	 * 我们在移动到下一个阶段之前，对所有索引处理每个阶段，
	 * 以提高效率。
	 */

	/*
	 * REINDEX CONCURRENTLY 的第 1 阶段
	 *
	 * 创建一个具有与旧索引相同属性的新索引，但它
	 * 仅在目录中注册，将在稍后构建。然后获取所有涉及表的
	 * 会话锁。有关更详细的注释，请参见 DefineIndex() 中的类似代码。
	 */

	foreach(fc_lc, fc_indexIds)
	{
		char	   *fc_concurrentName;
		ReindexIndexInfo *fc_idx = lfirst(fc_lc);
		ReindexIndexInfo *fc_newidx;
		Oid			fc_newIndexId;
		Relation	fc_indexRel;
		Relation	fc_heapRel;
		Oid			fc_save_userid;
		int			fc_save_sec_context;
		int			fc_save_nestlevel;
		Relation	fc_newIndexRel;
		LockRelId  *fc_lockrelid;
		Oid			fc_tablespaceid;

		fc_indexRel = index_open(fc_idx->indexId, ShareUpdateExclusiveLock);
		fc_heapRel = table_open(fc_indexRel->rd_index->indrelid,
							 ShareUpdateExclusiveLock);

		/*
		 * 切换到表所有者的用户 ID，以便任何索引函数都
		 * 作为该用户运行。同时锁定受限的安全操作
		 * 并安排使 GUC 变量更改对该命令本地化。
		 */
		GetUserIdAndSecContext(&fc_save_userid, &fc_save_sec_context);
		SetUserIdAndSecContext(fc_heapRel->rd_rel->relowner,
							   fc_save_sec_context | SECURITY_RESTRICTED_OPERATION);
		fc_save_nestlevel = NewGUCNestLevel();

		/* 确定该索引在 set_indexsafe_procflags 中的安全性 */
		fc_idx->safe = (RelationGetIndexExpressions(fc_indexRel) == NIL &&
					 RelationGetIndexPredicate(fc_indexRel) == NIL);
		fc_idx->tableId = RelationGetRelid(fc_heapRel);
		fc_idx->amId = fc_indexRel->rd_rel->relam;

		/* 此函数不应在临时关系上调用。 */
		if (fc_indexRel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
			elog(ERROR, "cannot reindex a temporary table concurrently");

		pgstat_progress_start_command(PROGRESS_COMMAND_CREATE_INDEX,
									  fc_idx->tableId);

		fc_progress_vals[0] = PROGRESS_CREATEIDX_COMMAND_REINDEX_CONCURRENTLY;
		fc_progress_vals[1] = 0;	/* 初始化中 */
		fc_progress_vals[2] = fc_idx->indexId;
		fc_progress_vals[3] = fc_idx->amId;
		pgstat_progress_update_multi_param(4, fc_progress_index, fc_progress_vals);

		/* 为新索引选择一个临时关系名称 */
		fc_concurrentName = ChooseRelationName(get_rel_name(fc_idx->indexId),
											NULL,
											"ccnew",
											get_rel_namespace(fc_indexRel->rd_index->indrelid),
											false);

		/* 选择新的表空间，TOAST 表的索引不被移动 */
		if (OidIsValid(fc_params->tablespaceOid) &&
			fc_heapRel->rd_rel->relkind != RELKIND_TOASTVALUE)
			fc_tablespaceid = fc_params->tablespaceOid;
		else
			fc_tablespaceid = fc_indexRel->rd_rel->reltablespace;

		/* 基于给定索引创建新的索引定义 */
		fc_newIndexId = index_concurrently_create_copy(fc_heapRel,
													fc_idx->indexId,
													fc_tablespaceid,
													fc_concurrentName);

		/*
		 * 现在打开新索引的关系，还需要在其上加一个会话级锁。
		 */
		fc_newIndexRel = index_open(fc_newIndexId, ShareUpdateExclusiveLock);

		/*
		 * 在私有上下文中保存 OID 和锁的列表
		 */
		fc_oldcontext = MemoryContextSwitchTo(fc_private_context);

		fc_newidx = palloc(sizeof(ReindexIndexInfo));
		fc_newidx->indexId = fc_newIndexId;
		fc_newidx->safe = fc_idx->safe;
		fc_newidx->tableId = fc_idx->tableId;
		fc_newidx->amId = fc_idx->amId;

		fc_newIndexIds = lappend(fc_newIndexIds, fc_newidx);

		/*
		 * 保存 lockrelid 以保护每个关系免于被删除，然后关闭
		 * 关系。父关系上的 lockrelid 不在这里获取，以避免在同一关系上
		 * 多次加锁，而是依赖先前构建的 parentRelationIds。
		 */
		fc_lockrelid = palloc(sizeof(*fc_lockrelid));
		*fc_lockrelid = fc_indexRel->rd_lockInfo.lockRelId;
		fc_relationLocks = lappend(fc_relationLocks, fc_lockrelid);
		fc_lockrelid = palloc(sizeof(*fc_lockrelid));
		*fc_lockrelid = fc_newIndexRel->rd_lockInfo.lockRelId;
		fc_relationLocks = lappend(fc_relationLocks, fc_lockrelid);

		MemoryContextSwitchTo(fc_oldcontext);

		index_close(fc_indexRel, NoLock);
		index_close(fc_newIndexRel, NoLock);

		/* 回滚索引函数执行的任何 GUC 更改 */
		AtEOXact_GUC(false, fc_save_nestlevel);

		/* 恢复 userid 和安全上下文 */
		SetUserIdAndSecContext(fc_save_userid, fc_save_sec_context);

		table_close(fc_heapRel, NoLock);
	}

	/*
	 * 保存堆锁，以便后续的可见性检查与其他后端
	 * 可能与此会话冲突。
	 */
	foreach(fc_lc, fc_heapRelationIds)
	{
		Relation	fc_heapRelation = table_open(lfirst_oid(fc_lc), ShareUpdateExclusiveLock);
		LockRelId  *fc_lockrelid;
		LOCKTAG    *fc_heaplocktag;

		/* 在私有上下文中保存锁列表 */
		fc_oldcontext = MemoryContextSwitchTo(fc_private_context);

		/* 将堆关系的 lockrelid 添加到锁定关系的列表中 */
		fc_lockrelid = palloc(sizeof(*fc_lockrelid));
		*fc_lockrelid = fc_heapRelation->rd_lockInfo.lockRelId;
		fc_relationLocks = lappend(fc_relationLocks, fc_lockrelid);

		fc_heaplocktag = (LOCKTAG *) palloc(sizeof(LOCKTAG));

		/* 为此父关系在等待阶段保存 LOCKTAG */
		SET_LOCKTAG_RELATION(*fc_heaplocktag, fc_lockrelid->dbId, fc_lockrelid->relId);
		fc_lockTags = lappend(fc_lockTags, fc_heaplocktag);

		MemoryContextSwitchTo(fc_oldcontext);

		/* 关闭堆关系 */
		table_close(fc_heapRelation, NoLock);
	}

	/* 给每个表获取一个会话级别的锁。 */
	foreach(fc_lc, fc_relationLocks)
	{
		LockRelId  *fc_lockrelid = (LockRelId *) lfirst(fc_lc);

		LockRelationIdForSession(fc_lockrelid, ShareUpdateExclusiveLock);
	}

	PopActiveSnapshot();
	CommitTransactionCommand();
	StartTransactionCommand();

	/*
	 * 因为我们在此事务中不进行快照，所以不需要
	 * 在这里设置 PROC_IN_SAFE_IC 标志。
	 */

	/*
	 * REINDEX CONCURRENTLY 的第 2 阶段
	 *
	 * 为每个索引在一个单独的事务中构建新的索引，以避免
	 * 不必要地长时间打开事务。但是在执行之前，
	 * 等待直到没有正在运行的事务能够用旧的索引列表打开
	 * 索引的表。有关更多详细信息，请参见 DefineIndex() 中的
	 * “第 2 阶段”。
	 */

	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
								 PROGRESS_CREATEIDX_PHASE_WAIT_1);
	WaitForLockersMultiple(fc_lockTags, ShareLock, true);
	CommitTransactionCommand();

	foreach(fc_lc, fc_newIndexIds)
	{
		ReindexIndexInfo *fc_newidx = lfirst(fc_lc);

		/* 为此索引的并发构建启动新的事务 */
		StartTransactionCommand();

		/*
		 * 检查用户请求的中止。这是在事务内部，因此
		 * xact.c 不会发出无用的 WARNING，并确保在中止时
		 * 会话级别的锁被清理。
		 */
		CHECK_FOR_INTERRUPTS();

		/* 告诉并发索引忽略我们，如果索引合格 */
		if (fc_newidx->safe)
			fc_set_indexsafe_procflags();

		/* 设置ActiveSnapshot，因为索引中的函数可能需要它 */
		PushActiveSnapshot(GetTransactionSnapshot());

		/*
		 * 更新待构建索引的进度，涉及正确的父表。
		 */
		pgstat_progress_start_command(PROGRESS_COMMAND_CREATE_INDEX, fc_newidx->tableId);
		fc_progress_vals[0] = PROGRESS_CREATEIDX_COMMAND_REINDEX_CONCURRENTLY;
		fc_progress_vals[1] = PROGRESS_CREATEIDX_PHASE_BUILD;
		fc_progress_vals[2] = fc_newidx->indexId;
		fc_progress_vals[3] = fc_newidx->amId;
		pgstat_progress_update_multi_param(4, fc_progress_index, fc_progress_vals);

		/* 执行新索引的并发构建 */
		index_concurrently_build(fc_newidx->tableId, fc_newidx->indexId);

		PopActiveSnapshot();
		CommitTransactionCommand();
	}

	StartTransactionCommand();

	/*
	 * 因为我们在此事务中不进行快照或 Xid，所以不需要
	 * 在这里设置 PROC_IN_SAFE_IC 标志。
	 */

	/*
	 * REINDEX CONCURRENTLY 的第 3 阶段
	 *
	 * 在此阶段，旧索引追赶在上一阶段
	 * 中创建的任何新元组。有关更多详细信息，请参见 DefineIndex()
	 * 中的“第 3 阶段”。
	 */

	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
								 PROGRESS_CREATEIDX_PHASE_WAIT_2);
	WaitForLockersMultiple(fc_lockTags, ShareLock, true);
	CommitTransactionCommand();

	foreach(fc_lc, fc_newIndexIds)
	{
		ReindexIndexInfo *fc_newidx = lfirst(fc_lc);
		TransactionId fc_limitXmin;
		Snapshot	fc_snapshot;

		StartTransactionCommand();

		/*
		 * 检查用户请求的中止。这是在事务内部，因此
		 * xact.c 不会发出无用的 WARNING，并确保在中止时
		 * 会话级别的锁被清理。
		 */
		CHECK_FOR_INTERRUPTS();

		/* 告诉并发索引忽略我们，如果索引合格 */
		if (fc_newidx->safe)
			fc_set_indexsafe_procflags();

		/*
		 * 获取“参考快照”，将用于 validate_index()
		 * 过滤候选元组。
		 */
		fc_snapshot = RegisterSnapshot(GetTransactionSnapshot());
		PushActiveSnapshot(fc_snapshot);

		/*
		 * 更新待构建索引的进度，涉及正确的父表。
		 */
		pgstat_progress_start_command(PROGRESS_COMMAND_CREATE_INDEX,
									  fc_newidx->tableId);
		fc_progress_vals[0] = PROGRESS_CREATEIDX_COMMAND_REINDEX_CONCURRENTLY;
		fc_progress_vals[1] = PROGRESS_CREATEIDX_PHASE_VALIDATE_IDXSCAN;
		fc_progress_vals[2] = fc_newidx->indexId;
		fc_progress_vals[3] = fc_newidx->amId;
		pgstat_progress_update_multi_param(4, fc_progress_index, fc_progress_vals);

		validate_index(fc_newidx->tableId, fc_newidx->indexId, fc_snapshot);

		/*
		 * 我们现在可以摆脱活跃快照，但仍然需要保存
		 * xmin 限制以等待较旧的快照。
		 */
		fc_limitXmin = fc_snapshot->xmin;

		PopActiveSnapshot();
		UnregisterSnapshot(fc_snapshot);

		/*
		 * 为确保没有死锁，我们必须提交并开始另一个
		 * 事务，并在任何快照被获取之前进行等待。
		 */
		CommitTransactionCommand();
		StartTransactionCommand();

		/*
		 * 该索引现在是有效的，因为它包含所有当前
		 * 有趣的元组。但由于它可能不包含在引用快照被获取之前
		 * 删掉的元组，因此我们必须等待任何可能拥有较旧快照的
		 * 事务结束。
		 *
		 * 因为我们在这个事务中没有获取快照或Xid，
		 * 所以这里没有必要设置PROC_IN_SAFE_IC标志。
		 */
		pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
									 PROGRESS_CREATEIDX_PHASE_WAIT_3);
		WaitForOlderSnapshots(fc_limitXmin, true);

		CommitTransactionCommand();
	}

	/*
	 * REINDEX CONCURRENTLY的第4阶段
	 *
	 * 现在新索引已经验证，交换每个新索引与其对应的旧索引。
	 *
	 * 我们同时将新索引标记为有效，将旧索引标记为无效，
	 * 以确保我们只从具有正确名称的索引中获得约束违反。
	 */

	StartTransactionCommand();

	/*
	 * 因为这个事务只进行目录操作而不进行任何
	 * 索引操作，所以我们可以在这里无条件地设置
	 * PROC_IN_SAFE_IC标志。
	 */
	fc_set_indexsafe_procflags();

	forboth(fc_lc, fc_indexIds, fc_lc2, fc_newIndexIds)
	{
		ReindexIndexInfo *fc_oldidx = lfirst(fc_lc);
		ReindexIndexInfo *fc_newidx = lfirst(fc_lc2);
		char	   *fc_oldName;

		/*
		 * 检查用户请求的中止。这是在事务内部，因此
		 * xact.c 不会发出无用的 WARNING，并确保在中止时
		 * 会话级别的锁被清理。
		 */
		CHECK_FOR_INTERRUPTS();

		/* 为旧索引选择关系名称 */
		fc_oldName = ChooseRelationName(get_rel_name(fc_oldidx->indexId),
									 NULL,
									 "ccold",
									 get_rel_namespace(fc_oldidx->tableId),
									 false);

		/*
		 * 用新索引交换旧索引。这也将新索引标记为
		 * 有效，将旧索引标记为无效。
		 */
		index_concurrently_swap(fc_newidx->indexId, fc_oldidx->indexId, fc_oldName);

		/*
		 * 使表的relcache无效，以便在此提交后
		 * 所有会话将刷新任何可能引用该索引的缓存计划。
		 */
		CacheInvalidateRelcacheByRelid(fc_oldidx->tableId);

		/*
		 * 在这里进行CCI，以便后续迭代看到
		 * 目录中的oldName，并可以为它们的oldName选择一个
		 * 非冲突的名称。否则，如果一个表有两个索引
		 * 的名称在前NAMEDATALEN减去几个字符时相等，
		 * 这可能会导致冲突。
		 */
		CommandCounterIncrement();
	}

	/* 提交此事务并使索引交换可见 */
	CommitTransactionCommand();
	StartTransactionCommand();

	/*
	 * 虽然如果所有索引合格我们可以设置PROC_IN_SAFE_IC，
	 * 但没有真正的需要，因为我们在等待完成后才获取Xid，
	 * 而这持续的时间非常短。
	 */

	/*
	 * REINDEX CONCURRENTLY的第5阶段
	 *
	 * 将旧索引标记为死亡。首先，我们必须等到没有正在
	 * 运行的事务可能正在使用索引进行查询。有关更多详细信息，
	 * 请参见index_drop()。
	 */

	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
								 PROGRESS_CREATEIDX_PHASE_WAIT_4);
	WaitForLockersMultiple(fc_lockTags, AccessExclusiveLock, true);

	foreach(fc_lc, fc_indexIds)
	{
		ReindexIndexInfo *fc_oldidx = lfirst(fc_lc);

		/*
		 * 检查用户请求的中止。这是在事务内部，因此
		 * xact.c 不会发出无用的 WARNING，并确保在中止时
		 * 会话级别的锁被清理。
		 */
		CHECK_FOR_INTERRUPTS();

		index_concurrently_set_dead(fc_oldidx->tableId, fc_oldidx->indexId);
	}

	/* 提交此事务以使更新可见。 */
	CommitTransactionCommand();
	StartTransactionCommand();

	/*
	 * 虽然如果所有索引合格我们可以设置PROC_IN_SAFE_IC，
	 * 但没有真正的需要，因为我们在等待完成后才获取Xid，
	 * 而这持续的时间非常短。
	 */

	/*
	 * REINDEX CONCURRENTLY的第6阶段
	 *
	 * 删除旧索引。
	 */

	pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE,
								 PROGRESS_CREATEIDX_PHASE_WAIT_5);
	WaitForLockersMultiple(fc_lockTags, AccessExclusiveLock, true);

	PushActiveSnapshot(GetTransactionSnapshot());

	{
		ObjectAddresses *fc_objects = new_object_addresses();

		foreach(fc_lc, fc_indexIds)
		{
			ReindexIndexInfo *fc_idx = lfirst(fc_lc);
			ObjectAddress fc_object;

			fc_object.classId = RelationRelationId;
			fc_object.objectId = fc_idx->indexId;
			fc_object.objectSubId = 0;

			add_exact_object_address(&fc_object, fc_objects);
		}

		/*
		 * 使用PERFORM_DELETION_CONCURRENT_LOCK，以便index_drop()使用
		 * 正确的锁级别。
		 */
		performMultipleDeletions(fc_objects, DROP_RESTRICT,
								 PERFORM_DELETION_CONCURRENT_LOCK | PERFORM_DELETION_INTERNAL);
	}

	PopActiveSnapshot();
	CommitTransactionCommand();

	/*
	 * 最后，释放表上的会话级锁。
	 */
	foreach(fc_lc, fc_relationLocks)
	{
		LockRelId  *fc_lockrelid = (LockRelId *) lfirst(fc_lc);

		UnlockRelationIdForSession(fc_lockrelid, ShareUpdateExclusiveLock);
	}

	/* 开始一个新的事务以正确完成处理 */
	StartTransactionCommand();

	/* 记录我们做过的事情 */
	if ((fc_params->options & REINDEXOPT_VERBOSE) != 0)
	{
		if (fc_relkind == RELKIND_INDEX)
			ereport(INFO,
					(errmsg("index \"%s.%s\" was reindexed",
							fc_relationNamespace, fc_relationName),
					 errdetail("%s.",
							   pg_rusage_show(&fc_ru0))));
		else
		{
			foreach(fc_lc, fc_newIndexIds)
			{
				ReindexIndexInfo *fc_idx = lfirst(fc_lc);
				Oid			fc_indOid = fc_idx->indexId;

				ereport(INFO,
						(errmsg("index \"%s.%s\" was reindexed",
								get_namespace_name(get_rel_namespace(fc_indOid)),
								get_rel_name(fc_indOid))));
				/* 不在这里显示 rusage，因为它不是每个索引的。 */
			}

			ereport(INFO,
					(errmsg("table \"%s.%s\" was reindexed",
							fc_relationNamespace, fc_relationName),
					 errdetail("%s.",
							   pg_rusage_show(&fc_ru0))));
		}
	}

	MemoryContextDelete(fc_private_context);

	pgstat_progress_end_command();

	return true;
}

/*
 * 插入或删除适当的 pg_inherits 元组，以使给定索引
 * 成为指定父索引的一个分区。
 *
 * 这也修正了受影响索引的 pg_depend 信息。
 */
void IndexSetParentIndex(Relation fc_partitionIdx, Oid fc_parentOid)
{
	Relation	fc_pg_inherits;
	ScanKeyData fc_key[2];
	SysScanDesc fc_scan;
	Oid			fc_partRelid = RelationGetRelid(fc_partitionIdx);
	HeapTuple	fc_tuple;
	bool		fc_fix_dependencies;

	/* 确保这是一个索引 */
	Assert(fc_partitionIdx->rd_rel->relkind == RELKIND_INDEX ||
		   fc_partitionIdx->rd_rel->relkind == RELKIND_PARTITIONED_INDEX);

	/*
	 * 扫描 pg_inherits，查找将我们的索引链接到某个父级的行。
	 */
	fc_pg_inherits = relation_open(InheritsRelationId, RowExclusiveLock);
	ScanKeyInit(&fc_key[0],
				Anum_pg_inherits_inhrelid,
				BTEqualStrategyNumber, F_OIDEQ,
				ObjectIdGetDatum(fc_partRelid));
	ScanKeyInit(&fc_key[1],
				Anum_pg_inherits_inhseqno,
				BTEqualStrategyNumber, F_INT4EQ,
				Int32GetDatum(1));
	fc_scan = systable_beginscan(fc_pg_inherits, InheritsRelidSeqnoIndexId, true,
							  NULL, 2, fc_key);
	fc_tuple = systable_getnext(fc_scan);

	if (!HeapTupleIsValid(fc_tuple))
	{
		if (fc_parentOid == InvalidOid)
		{
			/*
			 * 没有 pg_inherits 行，也不需要父级：在这种情况下无事可做。
			 */
			fc_fix_dependencies = false;
		}
		else
		{
			StoreSingleInheritance(fc_partRelid, fc_parentOid, 1);
			fc_fix_dependencies = true;
		}
	}
	else
	{
		Form_pg_inherits fc_inhForm = (Form_pg_inherits) GETSTRUCT(fc_tuple);

		if (fc_parentOid == InvalidOid)
		{
			/*
			 * 存在一个 pg_inherits 行，我们想要清除；执行清除。
			 */
			CatalogTupleDelete(fc_pg_inherits, &fc_tuple->t_self);
			fc_fix_dependencies = true;
		}
		else
		{
			/*
			 * 存在一个 pg_inherits 行。如果它是我们想要的，那么我们
			 * 就可以了；如果它不同，那就意味着目录损坏，
			 * 不应该出现。
			 */
			if (fc_inhForm->inhparent != fc_parentOid)
			{
				/* 意外：在这种情况下我们不应该被调用 */
				elog(ERROR, "bogus pg_inherit row: inhrelid %u inhparent %u",
					 fc_inhForm->inhrelid, fc_inhForm->inhparent);
			}

			/* 已经处于正确的状态 */
			fc_fix_dependencies = false;
		}
	}

	/* 完成 pg_inherits 的处理 */
	systable_endscan(fc_scan);
	relation_close(fc_pg_inherits, RowExclusiveLock);

	/* 如果向父级添加了索引分区，则设置 relhassubclass */
	if (OidIsValid(fc_parentOid))
	{
		LockRelationOid(fc_parentOid, ShareUpdateExclusiveLock);
		SetRelationHasSubclass(fc_parentOid, true);
	}

	/* 正确设置分区上的 relispartition */
	fc_update_relispartition(fc_partRelid, OidIsValid(fc_parentOid));

	if (fc_fix_dependencies)
	{
		/*
		 * 插入/删除 pg_depend 行。如果设置父级，则在父索引和表上添加 PARTITION
		 * 依赖；如果删除父级，则删除 PARTITION 依赖。
		 */
		if (OidIsValid(fc_parentOid))
		{
			ObjectAddress fc_partIdx;
			ObjectAddress fc_parentIdx;
			ObjectAddress fc_partitionTbl;

			ObjectAddressSet(fc_partIdx, RelationRelationId, fc_partRelid);
			ObjectAddressSet(fc_parentIdx, RelationRelationId, fc_parentOid);
			ObjectAddressSet(fc_partitionTbl, RelationRelationId,
							 fc_partitionIdx->rd_index->indrelid);
			recordDependencyOn(&fc_partIdx, &fc_parentIdx,
							   DEPENDENCY_PARTITION_PRI);
			recordDependencyOn(&fc_partIdx, &fc_partitionTbl,
							   DEPENDENCY_PARTITION_SEC);
		}
		else
		{
			deleteDependencyRecordsForClass(RelationRelationId, fc_partRelid,
											RelationRelationId,
											DEPENDENCY_PARTITION_PRI);
			deleteDependencyRecordsForClass(RelationRelationId, fc_partRelid,
											RelationRelationId,
											DEPENDENCY_PARTITION_SEC);
		}

		/* 使我们的更新可见 */
		CommandCounterIncrement();
	}
}

/*
 * IndexSetParentIndex 的子例程，用于将给定索引的 relispartition 标志更新为给定值。
 */
static void fc_update_relispartition(Oid fc_relationId, bool fc_newval)
{
	HeapTuple	fc_tup;
	Relation	fc_classRel;
	ItemPointerData fc_otid;

	fc_classRel = table_open(RelationRelationId, RowExclusiveLock);
	fc_tup = SearchSysCacheLockedCopy1(RELOID, ObjectIdGetDatum(fc_relationId));
	if (!HeapTupleIsValid(fc_tup))
		elog(ERROR, "cache lookup failed for relation %u", fc_relationId);
	fc_otid = fc_tup->t_self;
	Assert(((Form_pg_class) GETSTRUCT(fc_tup))->relispartition != fc_newval);
	((Form_pg_class) GETSTRUCT(fc_tup))->relispartition = fc_newval;
	CatalogTupleUpdate(fc_classRel, &fc_otid, fc_tup);
	UnlockTuple(fc_classRel, &fc_otid, InplaceUpdateTupleLock);
	heap_freetuple(fc_tup);
	table_close(fc_classRel, RowExclusiveLock);
}

/*
 * 在 MyProc->statusFlags 中设置 PROC_IN_SAFE_IC 标志。
 *
 * 在进行并发索引构建时，我们可以设置此标志
 * 告诉其他正在并发运行 CREATE
 * INDEX CONCURRENTLY 或 REINDEX CONCURRENTLY 的进程在
 * 等待并发快照时忽略我们。一方面，它
 * 避免了无谓等待一个不感兴趣的进程；
 * 但更重要的是，它在某些情况下避免了死锁。
 *
 * 这只能安全地针对不执行任何
 * 可能访问其他表的表达式的索引，因此索引不得是
 * 表达式索引或部分索引。调用者负责仅在
 * 该假设成立时调用此例程。
 *
 * （该标志在事务结束时自动重置，因此必须
 * 为每个事务设置。）
 */
static inline void fc_set_indexsafe_procflags(void)
{
	/*
	 * 这只能在 MyProc 中安装 xid 或 xmin 之前调用；
	 * 否则，多个进程可能会看到一个向后移动的 Xmin。
	 */
	Assert(MyProc->xid == InvalidTransactionId &&
		   MyProc->xmin == InvalidTransactionId);

	LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
	MyProc->statusFlags |= PROC_IN_SAFE_IC;
	ProcGlobal->statusFlags[MyProc->pgxactoff] = MyProc->statusFlags;
	LWLockRelease(ProcArrayLock);
}
